# Workflows

URL: /code/file-conventions/workflows · Id: `page:code/file-conventions/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](/business/nouns/process) folder holds one or more [Workflows](/business/nouns/workflow): each a [Trigger](/business/nouns/trigger) and the durable steps it starts. It implements [triggers](/workflows/triggers) and [schedules](/workflows/schedules).

```text
workflows/renewals/
├── index.mdx
├── notice-at-60-days.mdx      prose, code and tests in one file
└── weekly-unanswered.ts       code 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](/business/nouns/event), such as `on.Order.paid` |
| `on.Noun.state(fn)` | Trigger | Runs when a [Thing](/business/nouns/thing) enters a state |
| `every.day(fn)`, `every('weekday at 09:00', fn)` | Trigger | Runs on a schedule |
| `$` | client | Every [Verb](/business/nouns/verb), acting as the Process owner's [Worker](/business/nouns/worker) |
| `$.step`, `$.sleep`, `$.waitForEvent` | function | Durable steps and waits that survive restarts |

[`startup-builder/globals`](https://startupbuilder.dev/startup-builder/globals.d.ts) declares `on`, `every` and `$`; [`.sb/types/`](/code/file-conventions/generated#types-in-the-editor) references it, so a Workflow file needs no import.

| Key | Type | Holds |
| --- | --- | --- |
| `stakes` | `low \| high` | High stakes wait for an [Approval](/business/nouns/approval) before a one-way Verb |
| `metric` | [Metric](/business/nouns/metric) id | What this Workflow moves, when it differs from its Process |

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

### Leaseline
Leaseline declares a `Lease` [Noun](/business/nouns/noun). One file holds the description, the code and its test.

~~~mdx title="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](/code/builders/startup) compiles each heading or call into one Workflow.

- a `Workflow` and a `Trigger` [Version](/business/nouns/version)
- each run is an [Action](/business/nouns/action) pinned to the file's Version
- human and agentic steps become [Tasks](/business/nouns/task); a high-stakes step waits as an [Approval](/business/nouns/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](/code/file-conventions/verb-functions).
- The founder's form is the guide [automate a workflow](/workflows/automate-a-workflow).