Automation

Automate with workflows

Workflows are channel automations in a few lines of YAML: react to messages or emoji, run on a schedule, receive webhooks from your tools, pause for a human approval — and post the result right where your team works.

7 min read Buzz guide

Workflows are on by default. Nothing to enable — the page below applies as soon as you open the desktop app. If you cannot see them, check that they were not switched off on this device: open Settings → App → Experiments and make sure Workflows is on. Workflows are desktop-only for now.

Every team has little rituals a machine should be doing: flagging incidents, echoing CI results into the channel, chasing approvals, posting the standup thread. A workflow is that ritual written down — it lives in the channel it serves, runs with its creator's permissions, and every run leaves a visible trace you can audit.

Anatomy of a workflow

The building blocks

Triggers — what starts a run:

TriggerFires when…
on: message_postedany message lands in the channel. Optional filter, e.g. str_contains(trigger_text, "P0").
on: reaction_addedsomeone reacts — optionally only a specific emoji.
on: schedulea cron expression (UTC) or simple interval like "30m" elapses.
on: webhookan HTTP POST hits your workflow's unique /hooks/… URL — CI, monitoring, anything.

Actions — what a step can do:

ActionWhat it does
send_messagepost to the channel (or reply in the triggering thread with reply_in_thread: true). Text supports templates like {{trigger.author}}.
send_dmdirect-message someone — a pubkey or {{trigger.author}}.
add_reactionreact to the triggering message with an emoji.
request_approvalpause and wait for a named human (e.g. "@release-manager"), with a timeout (default 24h). It gates the remaining steps, not who may start the workflow — see below.
call_webhookHTTP POST out to any HTTPS endpoint — close the loop with your other tools.
set_channel_topic / delayupdate the topic; or wait ("5m", "1h") before the next step.

Never put a secret in a call_webhook header. A workflow is stored as an ordinary message in its channel, so anyone who can read the channel can read your workflow — headers included, in plain text. Buzz now refuses to save a workflow whose headers carry Authorization, Proxy-Authorization, Cookie or X-Api-Key, or any header whose value starts with Bearer or Basic, whatever the header is called. That check covers the obvious shapes, not every one — a token under a name of your own invention will still save, and still be readable by the channel. And it only inspects headers. The url and the request body are part of the same channel-readable workflow and are not checked at all, so a Slack incoming-webhook URL (the URL itself is the secret), a PagerDuty routing_key in the body, or a token in a query parameter will all save without complaint — and all are readable by everyone in the channel. A signed URL is no safer here than a header for the same reason. Prefer an endpoint that identifies the caller without a shared secret — mutual TLS, an IP allowlist, or a receiver you control that holds the real credential itself. If you already saved a workflow with a credential in it — in a header, the URL or the body — treat that token as public and rotate it.

An approval step is not a permission check. request_approval pauses the steps that come after it until a named person says yes. It does nothing about who can set the workflow running — anyone who can post the triggering message can start it, and every step before the approval has already run by the time anyone is asked. Put the approval before the step that matters, and do not rely on it to keep the wrong person out.

Before you start: connect the CLI

The buzz CLI signs everything with an identity, so it needs one before it can create workflows. Two ways to get connected:

  • Recommended — provision a governed identity: run buzz agent add <name> once. It provisions an identity through Ekam (no keys to handle), verifies it, and wires the CLI config for you. Run it in a new, empty folder for that identity, never in your home folder: it writes a .mcp.json there, and an assistant started in that folder acts as that identity.
  • Already have an identity? The CLI reads three environment variables: BUZZ_RELAY_URL (e.g. wss://ola.buzz.ola.in), BUZZ_PRIVATE_KEY, and BUZZ_AUTH_TAG (the owner delegation). Agents launched from the desktop app get these injected automatically.

Your identity must be a member of the channel you're adding the workflow to — join the channel in Buzz first. If buzz workflows create answers with membership required, that's the fix.

Your first workflow — five minutes

Workflows ship via the Buzz CLI today (a visual builder is on the roadmap). Save this as smoke.yaml:

name: Workflow smoke test
trigger:
  on: message_posted
  filter: 'str_contains(trigger_text, "ping workflow")'
steps:
  - id: ack
    action: add_reaction
    emoji: "white_check_mark"
  - id: reply
    action: send_message
    text: "Workflow engine is alive — triggered by {{trigger.author}}."
    reply_in_thread: true

Then, from a terminal with the Buzz CLI signed in:

# create it in a channel, list what's there, remove it later
# --yaml takes the definition itself, so feed the file in on stdin with -
buzz workflows create --channel <channel-id> --yaml - < smoke.yaml
buzz workflows list --channel <channel-id>
buzz workflows delete --workflow <workflow-id>

Post "ping workflow" in the channel — you'll get a ✅ on your message and a thread reply. That's the whole loop: trigger → steps → visible result.

Firing a webhook

on: webhook gives a workflow its own URL, so anything that can send an HTTP request — CI, monitoring, a cron box, another service — can start a run. The trigger is the easy half; this is the half you need to actually call it.

1. Get the secret

Every webhook workflow is protected by a secret. The relay generates it when the workflow is saved and returns it once. Creating from the CLI hands it straight back:

# the response carries BOTH ids you need
buzz workflows create --channel <channel-id> --yaml - < ci-status.yaml
# {"workflow_id":"542b6328-…","webhook_secret":"ee5f268b-…", …}

In the desktop app, save the workflow and use Copy Secret in the dialog that appears. If you lose the secret there is no way to read it back — re-save the workflow and the relay issues a new one.

2. Call it

POST to /hooks/<workflow-id>. Send the secret in the x-webhook-secret header — proxies and access logs routinely record query strings, and a header keeps it out of them. A ?secret= query parameter works as a fallback for callers that cannot set headers:

# preferred — secret in a header
curl -X POST https://<your-community-host>/hooks/<workflow-id> \
  -H 'content-type: application/json' \
  -H 'x-webhook-secret: <secret>' \
  -d '{"status":"passed","build":"4821"}'

# fallback — secret in the query string
curl -X POST 'https://<your-community-host>/hooks/<workflow-id>?secret=<secret>' \
  -H 'content-type: application/json' \
  -d '{"status":"passed","build":"4821"}'

Every top-level key of the JSON body becomes a trigger field, so {"status":"passed"} is available to your steps as {{trigger.status}}.

One trap, and it is why the examples above send status rather than text. Six field names are reserved for message and reaction triggers — author, text, channel_id, emoji, message_id, timestamp — and a built-in always wins over a payload key of the same name. A webhook has no message behind it, so those built-ins are empty: send {"text":"build passed"} and {{trigger.text}} renders as nothing at all, with no error anywhere. Name webhook payload fields something outside that list and they resolve normally.

3. What comes back

The call is accepted and the run happens asynchronously, so a success is 202 with a run id — not a result:

ResponseMeans
202 {"run_id":…,"status":"pending"}accepted. The run is queued; poll buzz workflows runs for its outcome.
401 {"error":"authentication failed"}wrong secret, or no secret sent.
400 {"error":"invalid workflow UUID"}the id in the path is not a UUID.
404 {"error":"workflow not found"}no such workflow on this host — including one that was deleted.

Two deliberate behaviours worth knowing. A workflow saved without a secret is refused rather than left open — re-save it to generate one. And the 404 is intentionally identical for an unknown host, an unknown workflow, and a workflow that exists in a different community, so a caller cannot use it to discover what exists elsewhere. Webhooks are bound to the community of the host you call, not to the workflow id alone.

Not just for developers

Everything below is the same four ideas — something happens, check a condition, do a few steps, tell people — applied to work that has nothing to do with code. The syntax is the same; you copy a recipe and change the words.

A · Welcome a new joiner

Someone posts that they have joined. They get a welcome and the right links, without anyone remembering to send them.

name: Welcome a new joiner
trigger:
  on: message_posted
  filter: 'str_contains(trigger_text, "joining")'
steps:
  - id: welcome
    action: send_message
    reply_in_thread: true
    text: "Welcome {{trigger.author}} — start here: https://buzz.ola.in/guides/getting-started.html"

B · Approvals that do not get lost

A request in the channel gets acknowledged, waits for a person to approve, and says what happened. Useful for travel, purchases, leave — anything that today is a DM someone forgets.

name: Travel request
trigger:
  on: message_posted
  filter: 'str_contains(trigger_text, "travel request")'
steps:
  - id: ack
    action: add_reaction
    emoji: "✈️"
  - id: approve
    action: request_approval
    from: "people-ops-lead"
    message: "Travel request from {{trigger.author}}: {{trigger.text}}"
    timeout: "48h"
  - id: confirm
    action: send_message
    reply_in_thread: true
    text: "Approved — people ops will book this."

C · The reminder nobody has to remember

A message on a schedule. Timesheets on Friday, a weekly report, the monthly all-hands nudge.

name: Friday timesheet nudge
trigger:
  on: schedule
  cron: "0 10 * * 5"
steps:
  - id: nudge
    action: send_message
    text: "Timesheets close at 6pm today."

D · Escalate what an emoji marks

Anyone marks a message 🚨. The workflow waits a few minutes to let the team catch it, then DMs whoever is on point.

name: Escalate urgent
trigger:
  on: reaction_added
  emoji: "🚨"
steps:
  - id: wait
    action: delay
    duration: "15m"
  - id: tell
    action: send_dm
    to: "on-call-lead"
    text: "Still open: {{trigger.text}}"

The variables you can use in text are {{trigger.author}}, {{trigger.text}}, {{trigger.emoji}}, {{trigger.channel_id}}, {{trigger.message_id}} and {{trigger.timestamp}}. Anything else stays on the page as written.

Recipes teams actually use

Every workflow on this page is run through the parser on each change. Copy one and it will save. That matters more than it sounds: the field names are not guessable from the action names — request_approval takes message, not prompt, and delay takes duration, not seconds — and a wrong one is only discovered when the save is refused.

1 · Incident intake

P0/P1 mentioned in the ops channel? Flag it instantly, open the escalation thread, and tell whoever raised it that it was picked up — no human required at 3am.

name: Incident intake
trigger:
  on: message_posted
  filter: 'str_contains(trigger_text, "P0") || str_contains(trigger_text, "P1")'
steps:
  - id: flag
    action: add_reaction
    emoji: "rotating_light"
  - id: thread
    action: send_message
    text: "Incident intake triggered. Please add impact + scope in this thread."
    reply_in_thread: true
  - id: tell_reporter
    action: send_dm
    to: "{{trigger.author}}"
    text: "Your P1 was picked up — the thread is open in the ops channel."

2 · CI results into the channel

Point GitHub Actions (or anything) at the workflow's webhook URL and the channel hears about every build — the pattern that replaces most old Slack integrations.

name: CI status
trigger:
  on: webhook
steps:
  - id: announce
    action: send_message
    text: "Build {{trigger.build}} on {{trigger.branch}}: {{trigger.status}}"

3 · Approval gate

The release request pauses until a named human says yes — then the deploy webhook fires. Approvals live in chat, with a timeout so nothing hangs forever.

name: Deploy approval
trigger:
  on: message_posted
  filter: 'str_contains(trigger_text, "deploy request")'
steps:
  - id: gate
    action: request_approval
    from: "@release-manager"
    message: "Deploy requested by {{trigger.author}} — approve?"
    timeout: "4h"
  - id: ship
    action: call_webhook
    url: "https://ci.example.internal/hooks/deploy"

4 · Daily standup thread

9:00 IST on weekdays, the prompt appears; the team replies in-thread. Nobody has to remember to post it.

name: Daily standup
trigger:
  on: schedule
  cron: "30 3 * * 1-5"  # 09:00 IST, Mon–Fri (cron is UTC)
steps:
  - id: prompt
    action: send_message
    text: "Standup time — yesterday / today / blockers. Reply in thread."

5 · Reaction-driven ops

A ✅ from anyone on a request message confirms it to the requester and notifies your ticket system — one emoji, two systems updated.

name: Approve by reaction
trigger:
  on: reaction_added
  emoji: "white_check_mark"
steps:
  - id: confirm
    action: send_dm
    to: "{{trigger.author}}"
    text: "Your request was approved ✅"

6 · Hand off to an agent

Workflows are deterministic plumbing; agents bring judgment. Let the workflow spot the moment and @mention the agent to do the thinking:

name: Lead triage
trigger:
  on: message_posted
  filter: 'str_contains(trigger_text, "new lead")'
steps:
  - id: handoff
    action: send_message
    text: "@Fizz please draft a first response for this lead."
    reply_in_thread: true

🎬 Video walkthrough — coming with the launch notes. It will show: ① writing smoke.yaml, ② buzz workflows create, ③ the ✅-and-reply firing live in a channel, ④ the approval-gate recipe end-to-end.

Good to know

  • A workflow runs with its creator's authority — and if the creator leaves the channel, the workflow is automatically disabled. No orphaned automations acting for people who've moved on.
  • Every run is traceable — trigger, each step, success or failure land as auditable events.
  • Conditions skip, they don't fail — an if: that's false just skips that step and moves on.
  • Today: CLI. Tomorrow: builder. Definitions are plain YAML your team can review like code; a visual builder is on the roadmap.

Pair this with Create your own agent — workflows for the predictable parts, agents for the judgment calls.