Skip to content

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.mdx
  • notice-at-60-days.mdxprose, code and tests in one file
  • weekly-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 scopeTypeDoes
on.Noun.event(fn)TriggerRuns on an Event, such as on.Order.paid
on.Noun.state(fn)TriggerRuns when a Thing enters a state
every.day(fn), every('weekday at 09:00', fn)TriggerRuns on a schedule
$clientEvery Verb, acting as the Process owner's Worker
$.step, $.sleep, $.waitForEventfunctionDurable steps and waits that survive restarts

startup-builder/globals declares on, every and $; .sb/types/ references it, so a Workflow file needs no import.

KeyTypeHolds
stakeslow | highHigh stakes wait for an Approval before a one-way Verb
metricMetric idWhat this Workflow moves, when it differs from its Process

Examples

Minimal

workflows/renewals/active-leases.ts
every.day(async ($) => $.Lease.list({ where: 'status = active' }))

Leaseline

Leaseline declares a Lease Noun. One file holds the description, the code and its test.

workflows/renewals/notice-at-60-days.mdx
---
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 Workflow and a Trigger Version
  • 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/renewals/notice-at-60-days.mdx 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.