--- title: 'Serial execution' description: 'Mark a workflow to run one at a time in FIFO order, so overlapping triggers queue up durably and start in arrival order instead of running in parallel.' position: 4 --- # Serial execution > By default a workflow's runs go in parallel: trigger the same workflow three > times and three runs start at once. Mark it **serial** and its runs go one at a > time, in arrival order. A trigger that arrives while a run is > active waits its turn in a durable queue and starts on its own the moment the > run ahead of it finishes. Serial mode is a property of the whole workflow. The one-at-a-time guarantee holds no matter what starts it: the CLI, an HTTP call, a schedule, or another workflow. ## When you need it Use serial mode when two copies of the same workflow must never run at the same time. For example: - It writes to a shared resource that two runs would corrupt. - It must process events in the exact order they arrived. - A second copy would double-process work the first is already doing. If runs are independent and safe to overlap, leave the workflow on the default (`concurrent`) and skip this page. ## Mark a workflow serial Set `executionMode: 'serial'` on the workflow in `stackbone.config.ts`. This is the one case where you declare a workflow in the config file instead of letting the convention scan find it. ```ts // stackbone.config.ts import { defineWorkspace } from '@stackbone/sdk'; export default defineWorkspace({ agents: [], workflows: [ { name: 'reconcile-ledger', module: 'workflows/reconcile-ledger.workflow.ts', export: 'reconcileLedgerWorkflow', executionMode: 'serial', // one run at a time, in arrival order }, ], }); ``` The workflow itself does not change. You still write `reconcile-ledger.workflow.ts` with its `'use workflow'` function and its `inputSchema` / `outputSchema` exports, as in [Overview](/docs/sdk/workflows/overview). **When this config file is present, its `workflows` list replaces the convention scan.** List every workflow you want the runtime to see, not only the serial one. Give the rest no `executionMode` (or `'concurrent'`), so they keep running in parallel: ```ts workflows: [ { name: 'reconcile-ledger', module: 'workflows/reconcile-ledger.workflow.ts', export: 'reconcileLedgerWorkflow', executionMode: 'serial' }, { name: 'send-digest', module: 'workflows/send-digest.workflow.ts', export: 'sendDigestWorkflow' }, // stays concurrent ], ``` See [project configuration](/docs/cli/reference/configuration) for the full `stackbone.config.ts` reference. ## What serial mode does A serial workflow runs **one execution at a time**. While a run is active, the runtime adds each new trigger to a durable per-workflow waiting list with its own input. The next trigger starts on its own, in arrival order, as soon as the active run reaches a terminal state. A run is terminal when it **completes, fails, or is cancelled**. All three release the lock and let the next trigger start, so a run that throws or gets cancelled never wedges the queue. The waiting list is durable, so it survives a runtime restart. Triggers queued before a crash are still there when the runtime comes back, in the same order. ## What you see while runs are queued A queued trigger shows up as a run with the status **`waiting`**. You see these in `stackbone dev` and in the Studio runs view, so you can spot a backed-up queue. A `waiting` row becomes a normal running run the moment its turn comes up. ## Triggering a serial workflow Triggering works the same as for any workflow, with two differences: - `stackbone.workflows.start(name, input)` returns `{ status: 'started', runId }` when it starts a run right away. If the workflow is serial and a run is already holding the lock, it returns `{ status: 'queued' }` with no `runId` yet. The trigger is on the waiting list and will start on its own. - `stackbone.workflows.startAndWait(name, input)` refuses a serial workflow. A queued trigger has no run to await, so the call fails fast with an error instead of skipping the queue. Use `start` and let the queue drain. ```ts import { stackbone } from '@stackbone/sdk'; const handle = await stackbone.workflows.start('reconcile-ledger', { day }); if (handle.status === 'queued') { // No run yet: it is waiting behind an active run and will start automatically. } else { // handle.runId is the run that just started. } ``` ## What's next - [Getting started](/docs/sdk/workflows/building-workflows): scaffold, trigger, and observe a workflow. - [Examples](/docs/examples/workflows/onboarding-pipeline): full workflows you can copy. - [Project configuration](/docs/cli/reference/configuration): the `stackbone.config.ts` reference where `executionMode` lives.