# Overview

Build event-driven automation in the console and drive it from your app by emitting events.

A workflow is a graph: one or more triggers, then actions and conditions wired
together. You build it in the console's visual editor. Your application's job is
to **produce the events** that start it.

Most triggers are system events that BuildBase already emits — a signup, a
payment, a form submission. You rarely call a workflow directly; you cause the
event it listens for.

```typescript
// A form submission emits form.submitted, which starts any workflow
// whose trigger is triggers.form.submitted. No workflow API call needed.
await fetch(`${BASE}/api/forms/public/${orgId}/${formId}/submit`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, message }),
});
```

> **Note:**
  Build the workflow in the BuildBase console under **Workflows**, then publish
  it. A workflow in `draft` status never runs, no matter how many matching
  events arrive.


## Lifecycle

A workflow moves through four states:

| Status     | Behavior                                                           |
| ---------- | ------------------------------------------------------------------ |
| `draft`    | Editable. Does not run                                             |
| `active`   | Published. Matching events create instances                        |
| `paused`   | Published but halted. See below for what happens to in-flight runs |
| `archived` | Retired                                                            |

Editing a published workflow does not change what is running. You edit a
**draft** of it and publish that draft as a new version, which is why
`/publish-preview` exists — it shows the diff before you commit.

Publishing validates the draft first, then hashes the flow. If the hash matches
the latest published version, nothing is written.

> **Note:**
  **Publish failures return HTTP `200`.** Both a validation failure and a
  no-change publish come back as `200` with `success: false`:

```json
{ "success": false, "message": "Workflow has validation errors", "data": { "errors": [] } }
{ "success": false, "message": "No changes to publish", "data": { "noChanges": true } }
```

Check the `success` field. Treating `res.ok` as "published" silently misses
both cases.



Each publish writes an immutable version record, so previous versions stay
readable after the flow changes. An optional `message` on the publish call is
kept as the change description, trimmed to 500 characters.

## Instances

Each time a trigger matches, an **instance** is created — one run of the graph
for one subject.

| Status      | Terminal |
| ----------- | -------- |
| `running`   | No       |
| `paused`    | No       |
| `completed` | Yes      |
| `failed`    | Yes      |
| `canceled`  | Yes      |

`paused` is deliberately not terminal — a paused instance is holding its
position and will continue from there when resumed.

Because instances are per-subject, pausing is available at several scopes: a
single instance, everything for one audience member, everything for one
workspace, or an organization-wide emergency stop.

## Re-entry

A subject that triggers the same workflow twice does not automatically get two
runs. Re-entry mode, set in the workflow's settings panel, decides:

| Mode       | Behavior                                                          |
| ---------- | ----------------------------------------------------------------- |
| `always`   | Default. Every matching event starts a new instance               |
| `once`     | The subject enters once — unless its previous instance **failed** |
| `cooldown` | Re-entry allowed after `cooldownMinutes`, defaulting to 60        |

`once` is right for onboarding sequences and wrong for anything recurring, like
a monthly usage warning.

Two details that decide whether a subject is actually blocked:

**A failed run does not count.** The `once` check ignores instances in `failed`
status, so a subject whose run failed can re-enter. That is usually what you
want — it makes a fixed workflow re-runnable — but it means `once` is not a
hard guarantee of exactly one execution.

> **Note:**
  **Re-entry needs a subject to key on.** The check resolves an identity from
  the event payload, trying `audienceId`, then `userId`, `actorId`,
  `targetUserId`, and finally `email`.

If the payload carries none of these, re-entry is **not enforced at all** and
every matching event starts a new instance, whatever the mode says. Workflows
triggered by events without a subject — most `triggers.workflow.*` events, for
example — cannot rely on `once` or `cooldown`.



Enforcement is a Redis lock acquired atomically, so two events arriving at the
same instant cannot both pass. A database check backs it up, which is what makes
`once` outlast the lock's own expiry.

## Retries

Every action node has a retry policy. Without a custom one it uses the default:

| Setting        | Default       | Allowed range            |
| -------------- | ------------- | ------------------------ |
| `maxAttempts`  | 3             | 1 – 10                   |
| `backoffType`  | `exponential` | `fixed` or `exponential` |
| `backoffDelay` | 5,000 ms      | 1,000 – 300,000 ms       |
| `timeoutMs`    | 30,000 ms     | 5,000 – 300,000 ms       |

[HTTP webhook actions](/workflows/webhook-actions) override this with a more
forgiving policy, because outbound HTTP is the flakiest thing a workflow does.

## Testing before publishing

`/test` runs a workflow in one of two modes:

- **`dry-run`** — walks the graph and reports what would happen. No email is
  sent, no credit is granted.
- **`hot-run`** — executes for real against a chosen subject.

Use `dry-run` to check branching logic and `hot-run` to check that an email
actually renders.

## Next Steps

- [Triggers and actions](/workflows/triggers-and-actions) — the full catalog.
- [Webhook actions](/workflows/webhook-actions) — call your own API from a workflow.
- [Monitoring](/workflows/monitoring) — instances, retries, and the dead-letter queue.
