Workflows
A file in a Process folder holds one or more Workflows, each a Trigger and the durable steps it starts, in MDX or TypeScript.
A file in a Process folder holds one or more Workflows: each a Trigger and the durable steps it starts. It implements triggers and schedules.
workflows/renewals/index.mdxnotice-at-60-days.mdxprose, code and tests in one fileweekly-unanswered.tscode only
Reference
Two spellings. In MDX, a heading names a Workflow and the paragraph under it describes it. A ts block runs, a ts example block is shown only, and a ts test block runs as a test.
In TypeScript, a file holds the same calls with no prose.
| In scope | Type | Does |
|---|---|---|
on.Noun.event(fn) | Trigger | Runs on an Event, such as on.Order.paid |
on.Noun.state(fn) | Trigger | Runs when a Thing enters a state |
every.day(fn), every('weekday at 09: | Trigger | Runs on a schedule |
$ | client | Every Verb, acting as the Process owner's Worker |
$.step, $.sleep, $.waitForEvent | function | Durable steps and waits that survive restarts |
startup-builder/ declares on, every and $; .sb/ references it, so a Workflow file needs no import.
| Key | Type | Holds |
|---|---|---|
stakes | low | high | High stakes wait for an Approval before a one-way Verb |
metric | Metric id | What this Workflow moves, when it differs from its Process |
Examples
Minimal
every.day(async ($) => $.Lease.list({ where: 'status = active' }))Leaseline
Leaseline declares a Lease Noun. One file holds the description, the code and its test.
---
stakes: low
---
## Send the notice at 60 days {#notice-at-60-days}
Each morning, every active Lease that ends in 60 days gets its renewal notice.
```ts
every.day(async ($) => {
const endsOn = new Date(Date.now() + 60 * 86_400_000).toISOString().slice(0, 10)
for await (const lease of $.Lease.list({ where: `status = active and endsOn = ${endsOn}` }).all())
await $.step(lease.$id, () => $.Lease.notify({ lease: lease.$id }))
})
```
```ts test
import { expect, run } from 'startup-builder/ test'
const { calls } = await run({}, { Lease: { list: () => [{ $id: 'lease_1', status: 'active' }], notify: () => ({}) } })
expect(calls).toEqual(['Lease.list', 'Lease.notify'])
```What it builds
The StartupBuilder compiles each heading or call into one Workflow.
- a
Workflowand aTriggerVersion - each run is an Action pinned to the file's Version
- human and agentic steps become Tasks; a high-stakes step waits as an Approval
Routing
workflows/ is $.renewals.noticeAt60Days, the MCP path renewals.noticeAt60Days. To run it by hand, call it from the SDK or MCP.
Good to know
- The
{#id}after a heading keeps the id across renames. - A Workflow on an imperative Verb, such as
on.Lease.renew, is that Verb's body and conflicts with a Verb function. - The founder's form is the guide automate a workflow.