
Workflows¶
A workflow is an ordered list of steps vamoose runs and the daemon advances. Run one with vamoose run <name> and list them with vamoose workflows.
Built-in workflows¶
| Name | Steps | Use |
|---|---|---|
pto |
hold shown free, approve, notify | Time off a manager approves. |
notify-only |
hold shown free, notify | Tell the team, no approval. |
away |
out-of-office block | Personal out of office, no fanout. |
request, off, check, and promote are short fronts over the pto workflow.
Custom workflows¶
Drop a JSON file in ~/.config/vamoose/workflows/<name>.json, or pipe it to vamoose workflows add (which validates it first). A user file overrides a built-in of the same name. Then run vamoose run <name>.
{
"name": "team-heads-up",
"description": "Hold shown free, tell the team, no approval.",
"steps": [
{ "verb": "hold", "showAs": "free" },
{ "verb": "notify", "team": "optional" }
]
}
Step fields¶
| Field | Applies to | Meaning |
|---|---|---|
verb |
all | The action (see below). Required. |
showAs |
hold, away, event | Free/busy: free, busy, tentative, oof. Defaults per verb. |
manager |
approve | Wait on the directory manager (or --manager). The first approver. |
approver |
approve | Name a later approver by email in a multi-approver chain. |
team |
notify | Role for the team. Must be optional. |
id |
any | Names the step so branches can target it. |
on |
approve | Branch by outcome: {"accepted": id, "declined": id, "expired": id}. |
timeout |
approve | Duration to wait (e.g. 72h) before the expired branch runs. |
when |
any but the first | Guard that skips the step unless its conditions hold. See Guards. |
next |
any | The step id to run next, or end. Defaults to the following step. |
subject |
note, event, message | Event title, or the text for a message step. |
channel |
message | Where a message step posts, such as a Slack channel. Required. |
for |
wait | How long a wait step pauses, as a duration like 48h. Required. |
Verbs¶
holdcreates the event shown free, inviting the manager when anapprovestep follows.approvewaits for an approver to accept the invite; chain several for multi-level approval.notifyadds the team as optional attendees.notecreates a short event on your own calendar to mark an outcome, such as a decline.awaymarks out of office with no attendees.eventcreates a plain event, with attendees from--attendees.canceldeletes the hold.messageposts to a comms channel, such as a Slack channel, to announce the outcome.waitpauses the workflow for a duration; the daemon advances past it once the time passes.
Rules¶
A workflow must:
- start with exactly one creating step (
hold,away, orevent) - put any
approvesteps in ahold-led workflow, naming later chain approvers withapprover - run
notifyafterapprove, never before - keep the
notifyteamoptional, so it never blocks a teammate's calendar
Run vamoose run <name> --dry-run to print the plan without touching the calendar.
Watching for approval¶
vamoose run <name> --watch records the hold at its approval gate. vamoose daemon then advances the workflow, notifying the team for pto, once the manager accepts.
Branching¶
An approve step can branch on its outcome with on, so a workflow does one thing when the manager accepts and another when they decline. Give steps an id and route with on and next. Absent branches fall through: accepted runs the next step, declined stops. Steps with no id, on, or next behave exactly as before, so linear workflows are unchanged.
The built-in pto-notify-on-decline shows it. On accept it notifies the team, on decline it notes the outcome on your calendar:
{
"name": "pto-notify-on-decline",
"steps": [
{ "id": "hold", "verb": "hold", "showAs": "free" },
{ "id": "approval", "verb": "approve", "manager": true,
"on": { "accepted": "notify", "declined": "denied" } },
{ "id": "notify", "verb": "notify", "team": "optional", "next": "end" },
{ "id": "denied", "verb": "note", "subject": "Time off declined", "next": "end" }
]
}
Timeouts¶
An approve step can set a timeout and an expired branch, so a workflow acts on its own when the manager never responds. The daemon runs the expired branch once the timeout passes with no accept or decline. The built-in pto-cancel-on-timeout cancels the hold after 72 hours of silence:
{
"name": "pto-cancel-on-timeout",
"steps": [
{ "id": "hold", "verb": "hold", "showAs": "free" },
{ "id": "approval", "verb": "approve", "manager": true, "timeout": "72h",
"on": { "accepted": "notify", "expired": "expired" } },
{ "id": "notify", "verb": "notify", "team": "optional", "next": "end" },
{ "id": "expired", "verb": "cancel", "next": "end" }
]
}
Delays¶
A wait step pauses a workflow for a set time, so it acts later without you rerunning it. Give it a for duration. Run the workflow with --watch and vamoose daemon advances past the wait once the time passes, then runs the following steps.
The built-in notify-later books the time now and tells the team a day later, rather than the instant it is held:
{
"name": "notify-later",
"steps": [
{ "id": "hold", "verb": "hold", "showAs": "free" },
{ "id": "wait", "verb": "wait", "for": "24h" },
{ "id": "notify", "verb": "notify", "team": "optional", "next": "end" }
]
}
A wait can sit before or after an approval, so a workflow can pause and then ask for sign-off, or hold after approval before fanning out. To repeat a whole workflow on a schedule, see Recurring below.
Recurring¶
vamoose schedule reruns a workflow on an interval, so a routine happens on its own. This differs from a wait step, which pauses a single run; a schedule starts a fresh run each interval. Give it a duration and a relative window that is resolved at each run:
vamoose schedule add pto --every 168h --phrase "next week" --manager boss@work.com
vamoose daemon fires each schedule once its interval passes, running the workflow for whatever the phrase resolves to at that moment, so next week always means the week ahead of the run. List them with vamoose schedule list and drop one with vamoose schedule remove <index>.
Intervals are Go durations, so weekly is 168h. Day-of-week and time-of-day schedules come in a later version.
Multiple approvers¶
Chain approve steps so a workflow needs more than one sign-off, in order. The first approve waits
on the manager (manager: true or --manager); each later approve names its approver by email with
approver, since the directory knows only the one manager. vamoose invites the next approver only
after the previous one accepts, so the director is not asked until the manager has signed off.
The built-in pto-two-level needs the manager, then a named director, before the team is told. Edit
the director email for your team:
{
"name": "pto-two-level",
"steps": [
{ "id": "hold", "verb": "hold", "showAs": "free" },
{ "id": "manager", "verb": "approve", "manager": true },
{ "id": "director", "verb": "approve", "approver": "director@example.com" },
{ "id": "notify", "verb": "notify", "team": "optional", "next": "end" }
]
}
Run it with --watch and vamoose daemon so each acceptance advances the chain automatically. A
decline at any gate stops the workflow, or takes that gate's declined branch if it has one.
Guards¶
A step can carry a when guard that gates whether it runs. When the conditions do not hold, the workflow skips the step and continues, so a guard trims a workflow rather than branching it. Guards layer on top of on: on chooses a path by the approval outcome, when gates any step on that path. The creating step always runs, so it cannot carry a guard.
Conditions are all optional and combine with and:
| Condition | Meaning |
|---|---|
dayOfWeek |
Run only on the named days, checked at execution time. |
minAttendees |
Run only when the hold has at least this many attendees. |
maxAttendees |
Run only when the hold has at most this many attendees. |
dayOfWeek is a comma-separated set of three-letter days or inclusive ranges, such as mon-fri or sat,sun. A range wraps past Saturday, so fri-mon is Friday through Monday. The attendee counts read the hold's invitees when the step runs, so an event that invites a crowd can trigger a follow-on step a small one skips.
The built-in pto-notify-weekdays approves time off but tells the team only on weekdays, so an approval that lands over the weekend does not page the team:
{
"name": "pto-notify-weekdays",
"steps": [
{ "verb": "hold", "showAs": "free" },
{ "verb": "approve", "manager": true },
{ "verb": "notify", "team": "optional", "when": { "dayOfWeek": "mon-fri" } }
]
}
A skipped step does not run later; the guard drops it for this run.
Messages¶
A message step posts to a comms channel, such as a Slack channel, so a workflow announces
its outcome where the team already is, not only on the calendar. Set a channel on the step.
The message text is the step's subject, or the hold's subject when the step sets none, so an
announcement carries the run's context without the workflow hardcoding it.
Messaging needs a comms backend. For Slack, create a bot token with the chat:write scope and
export it:
export VAMOOSE_SLACK_BOT_TOKEN=xoxb-...
For email, set your SMTP server. A channel that is an email address is sent as an email:
export VAMOOSE_SMTP_HOST=smtp.example.com
export VAMOOSE_SMTP_PORT=587
export VAMOOSE_SMTP_USERNAME=you@example.com
export VAMOOSE_SMTP_PASSWORD=...
export VAMOOSE_SMTP_FROM=vamoose@example.com
When both are set, a message routes by its channel: an address containing @ goes to email, anything else to Slack. So one workflow can post to a Slack channel and also email a list.
The built-in pto-announce approves time off, announces it to a channel, then notifies the team:
{
"name": "pto-announce",
"steps": [
{ "id": "hold", "verb": "hold", "showAs": "free" },
{ "id": "approval", "verb": "approve", "manager": true, "on": { "accepted": "announce" } },
{ "id": "announce", "verb": "message", "channel": "#out-of-office", "next": "notify" },
{ "id": "notify", "verb": "notify", "team": "optional", "next": "end" }
]
}