Workflow routines
Routines described as a chain of declared stages (code, model, skill, gate), as packs do since 0.6.0. The stage schema, what validation refuses, when to choose the shape, and content-calendar in full.
What it is
A workflow routine is written as an ordered list of stages, each with a kind and a one-line job. Packs describe every routine this way, and it is the natural shape for a routine ported from an n8n, Zapier or Make workflow.
There are four stage kinds:
| Kind | Who does it | Example |
|---|---|---|
model |
The model, with any connectors the stage names | Search, read, label, draft |
code |
A script, run by one exact command | routineos.py write, lib/check_citations.py |
skill |
The model, following a named skill | Draft in the house style |
gate |
A person, through an approval | Approve each post |
The stage list is a description, not an engine. The prompt still carries the steps and the run follows the prompt. There is no separate workflow runner in RoutineOS: stages exist so a person can read the routine’s shape, and so pack validate and pack install can check each step’s connectors and writes before anything runs.
Why it exists
A list of stages is easy to read, review and port. It makes the mix visible (which steps are code, which are the model, where the gate sits) without reading the whole prompt. It also gives validation something to hold on to: a stage that writes externally must sit behind a gate, a code stage must name a script the pack ships, and a skill stage must ship its skill.
When to choose it
Write the stage list first when:
- You are porting an existing workflow. Each node becomes a stage; see n8n workflow to routine.
- The routine mixes all four kinds and a reviewer needs to see where each sits.
- You will share the routine as a pack. Every pack has stages, from 1 to 20.
A workflow is a way of describing a routine, not an alternative to the other kinds. content-calendar below is a workflow, a skill routine and a hybrid at once. A routine you never share as a pack can skip the stage list and keep the same split in its prompt.
Use cases
| Use case | Stages | Shipped example |
|---|---|---|
| Search, record, learn, write, approve | model, model, code, code, gate | jobs-watch (piloted) |
| Read, label, draft, approve, send | model, model, skill, code, code, gate, model | inbox-triage (unpiloted) |
| Research, draft, check, approve, publish | model, skill, code, code, code, gate, model | content-calendar (unpiloted) |
| Collect, compare, audit, report | model, code, model, code, code | weekly-marketing-report (unpiloted) |
How it works
The stage schema
Stages are declared in a pack’s stages array, 1 to 20 entries, in run order.
| Field | Required | Notes |
|---|---|---|
id |
yes | Lowercase letters, digits and hyphens; unique in the pack |
kind |
yes | code, model, skill or gate |
title |
yes | Short name, up to 80 characters |
does |
yes | One plain sentence, up to 500 characters |
connectors |
no | Connector ids from the pack’s connectors; each connector must list this stage back |
reads, writes |
no | Store collections; never a people collection |
command |
code only | One plain line starting python3, naming routineos.py or a lib/*.py file the pack ships |
skill |
skill only | A skill under skills/<name>/SKILL.md in the pack’s files |
Any other key is refused. A command on a non-code stage, or a skill on a non-skill stage, is refused. See Pack schema for the whole file.
What validation refuses
pack validate (and pack install, which validates first) checks the stages against the rest of the pack. These are the real messages, from a copy of content-calendar with its gate switched off, a pipe added to a command, and decisions added to a write:
FAIL routine: action 'publish-post' (kind post): gate required, set [gate] enabled = true
FAIL stages[2]: command must be a single plain line with no shell operators
FAIL stages[4].writes: 'decisions' is a people collection; only people write it
FAIL stages[5]: a gate stage needs routine.gate.enabled = true
FAIL connectors[2]: a write connector needs routine.gate.enabled = true
FAIL connectors[3]: a write connector needs routine.gate.enabled = true
In words:
- A gate stage needs a gate.
routine.gate.enabledmust be true. - A write connector needs a gate. Any connector with
access = "write"is refused without one. - People collections are off limits. A stage that reads or writes
decisionsorsettingsis refused; only people write those. - Connectors and stages agree both ways. A stage that names a connector the connector does not list back, a connector that names an unknown stage, or a required connector no stage uses, is refused.
- Commands are narrow. No shell operators, no
-mor-c, and onlyroutineos.pyor a shippedlib/*.pyfile. - Skills ship. A skill stage whose
skills/<name>/SKILL.mdis missing fromfiles, or whose frontmatternamediffers, is refused.
From stages to prompt
Each stage should map to one or more numbered steps in prompt.md, in the same order. The stages say what happens; the prompt says how, with exact commands for code stages and the skill’s name for skill stages. Two stages appear in every shipped pack, in this order: learn, then write. The prompt appends learn’s proposals to the front of the run’s inbox between them, because proposals added after the write are lost.
How it compares
| Code | Skill | Workflow | Hybrid | |
|---|---|---|---|---|
| What it describes | Where the work runs | Where the judgement is written | The order of steps | The split between model and code |
| Checked before a run | Commands, in a pack | Frontmatter, by lint |
Every stage, by pack validate |
Settings filters, by write at run time |
| Required for a pack | No | No | Yes | No |
Worked example
content-calendar drafts posts on a set of topics, checks that every claim has a source, and publishes only what a person approves. It runs Monday and Wednesday at 21:00 UTC (schedule = "0 21 * * 1,3", type approval-gated), and it is unpiloted. It uses every stage kind:
| Stage | Kind | Does | Connectors |
|---|---|---|---|
research |
model | Reads the feeds and searches each topic, keeping every source URL | web-search, web |
draft |
skill | Drafts each post in each format in the house style, with a marker per claim (house-style) |
|
cite |
code | python3 lib/check_citations.py --draft draft.md --sources sources.json |
|
learn |
code | routineos.py learn |
|
write |
code | routineos.py write; writes runs, entities, inbox and pending |
|
approve |
gate | A person approves, edits or declines each draft; the gate times out after 72 hours | gate-mail |
publish |
model | On a later run, publishes only approved drafts, exactly as approved | cms |
Reading it as a reviewer:
- Where the model judges.
researchanddraft. The draft is shaped byskills/house-style/SKILL.md, so the voice changes by editing that file. - Where code holds the line.
citechecks every citation marker has a source and every source is cited; the model cannot wave a claim through.learnandwritekeep the store contract. - Where a person decides.
approve. The two write connectors,gate-mailandcms, are both optional, and both are allowed only because the gate is on. - What sends. Only
publish, and only on a later run, only for drafts whose decision is approved. Itscmsconnector lists “Copy the approved draft by hand from the Desk” as the alternative, so the routine works with no CMS connected.
Install it and check it:
python3 skills/routineos/scripts/routineos.py pack validate site/templates/content-calendar.json
python3 skills/routineos/scripts/routineos.py pack install site/templates/content-calendar.json --dir content-calendar
The install writes lib/check_citations.py and skills/house-style/SKILL.md with the routine files, and prints the connector report. It connects nothing and runs nothing.
For a piloted example, read jobs-watch as stages in Hybrid routines.
Common pitfalls
- Expecting stages to run themselves. The prompt runs; stages are the declared shape. A stage with no matching prompt step does nothing.
- Stages and prompt drifting apart. Reorder one, reorder the other. Reviewers read the stages and trust them.
- A write connector without a gate. Refused, even when the connector is optional.
- A stage that writes
decisionsorsettings. People own those; refused. - A shell pipeline in a code stage. Refused. Put the logic in a
lib/script and call it once. - Learning after write. Proposals appended after the write are lost. Keep
learnbeforewrite. - Porting every node. An n8n “Set” or “Merge” node is usually part of a model or code stage, not a stage of its own.