---
title: Workflows · BuzzKit
description: Event-triggered automation with waits, branches and sends, run per subscriber.
canonical: https://buzzkit.dev/features/workflows
last-updated: 2026-09-02
---

# Automation that reads like a spec. Trigger on an event, wait, branch, send.

A workflow is a versioned document: a trigger, optional conditions and steps that run for one subscriber at a time. Write it in the dashboard or send it to the API, dry-run it, then publish.

## A workflow is a document

A trigger, optional conditions and steps, written as JSON and versioned like code. Publish a version and it keeps running while you draft the next one. The dashboard draws the same document as a flow.

```
{
  // An event, or a schedule over a segment
  "trigger": { "event": "trial.started" },

  // One run at a time per subscriber
  "concurrency": "one-per-subscriber",

  // Events that cancel a live run
  "cancelOn": [{ "event": "subscription.started" }],

  // Steps run top to bottom, each with a name
  "steps": [
    { "name": "settle", "wait": "1d" },
    {
      "name": "quiet",
      "waitFor": {
        "event": "$app.backgrounded",
        "timeout": "1d"
      }
    },
    {
      "name": "plan",
      "branch": [
        {
          "name": "pro",
          "when": {
            "ref": "subscriber.attributes.plan",
            "eq": "pro"
          },
          "steps": []
        },
        { "name": "free", "steps": [] }
      ]
    },
    {
      "name": "nudge",
      "send": {
        "topic": "gym-reminders",
        "title": "Trial ends tomorrow"
      }
    }
  ]
}
```

## Three kinds of waiting

A step can wait for a duration, for a moment on the subscriber’s clock, or for an event. With a settle window, the app going to the background starts a clock, opening it resets it, and the step completes once it runs out. Time becomes a step, so a workflow follows how people actually use the app.

```
{ "name": "quiet", "waitFor": {
    "event": "$app.backgrounded",
    "settleFor": "5m",
    "resetOn": ["$app.opened"],
    "timeout": "1d"
} }
```

## Branches, loops and fetches

Branch on the subscriber’s attributes, on what they did, or on a reply from your own API. Loops repeat steps until a condition holds, and a fetch step calls your backend with a secret from the vault.

```
{
  "name": "plan",
  "branch": [
    {
      "name": "pro",
      "when": {
        "ref": "subscriber.attributes.plan",
        "eq": "pro"
      },
      "steps": []
    },
    {
      // A lane without a condition catches the rest
      "name": "free",
      "steps": [
        {
          "name": "offer",
          "fetch": {
            "url": "https://api.example.com/offers",
            "headers": {
              "Authorization": "Bearer {{ secrets.api }}"
            },
            "as": "offer"
          }
        },
        {
          // Up to three reminders, three days apart,
          // until the app is opened
          "name": "remind",
          "repeat": {
            "every": "3d",
            "max": 3,
            "until": {
              "occurred": "$app.opened",
              "since": "trigger"
            },
            "steps": []
          }
        }
      ]
    }
  ]
}
```

## Dry runs before every publish

Test any version against a real subscriber or a made-up one. Waits resolve instantly, sends render without sending, and assumed replies stand in for your API.

```
POST /v1/workflows/trial-nudge/test
{
  "externalId": "user_42",
  "event": {
    "name": "trial.started",
    "data": { "plan": "monthly" }
  },
  "assume": {
    "status": {
      "status": 200,
      "data": { "canceled": false }
    }
  }
}
```

## Capabilities

- **Versioned specs.** Every change is a new version, and the published one keeps running.
- **One run per subscriber.** Runs live on a Durable Object per subscriber, so ordering is exact.
- **Schedules too.** A trigger can be a cron or a daily time over a segment.
- **Cancel rules.** An event such as a purchase cancels the live run.
- **Local notifications.** A send can fire on the device as a local notification, even offline.
- **Full run history.** Every step, wait and send lands on the subscriber’s event stream.

## Questions

### What can start a workflow?

Any event your app or backend tracks, an event that arrives through a source such as Stripe, or a schedule over a segment. Each start runs for one subscriber.

### Can a step wait for the user to do something?

Yes. A step can wait for an event, with a timeout, and match on the event’s data. It can also wait for a quiet moment, such as five minutes after the app went to the background.

### What if the user does the thing before the reminder goes out?

Give the workflow cancel rules. When one of those events arrives, the live run stops and nothing more is sent.

### Can I test a workflow before publishing?

Yes. A dry run walks any version against a real or made-up subscriber, resolves every wait instantly and renders the sends without sending them.

## Related

- [Segments](https://buzzkit.dev/features/segments.md): Saved expressions over attributes and events, evaluated fresh at send time.
- [Scheduling](https://buzzkit.dev/features/scheduling.md): Hold a message for a moment, in one timezone or in every subscriber’s own.
- [Sending](https://buzzkit.dev/features/sending.md): One POST sends to a subscriber, a topic or a segment and lands on every device.

## Start

- [Start sending](https://buzzkit.dev/dashboard)
- [API Reference](https://docs.buzzkit.dev)
- [BuzzKit on GitHub](https://github.com/buzzkit-dev/buzzkit)
