Workflow guide

Everything a workflow can be made of, and everything it can be made to do — with 64 worked examples.

New to workflows? Read Part 1 in order — it assumes nothing and builds up.

Writing workflow JSON by hand, or through the API or MCP? Skip to the reference. Every field, limit and enum is there.

1. What a workflow is

A template gives you one perspective on an article. A workflow runs several steps over that article and combines them, so later steps can build on what earlier ones found.

Every workflow has the same three parts:

  • The seed. The thing being analysed — a web page, an uploaded PDF, the clipboard, or text you type. Every Template step that has nothing wired into it reads the seed.
  • The steps. One to a thousand of them (a hundred once published). Each produces an answer. Steps can feed each other, and steps whose inputs are ready run at the same time.
  • The final answer. One last step that receives whatever reaches it and writes the response you actually read.

A workflow is a graph, not a list. Nothing runs "in order" — a step runs as soon as its inputs exist. Three unwired steps all start at once. Order comes from wiring and conditions, and from nothing else.

Only a Template step reads the article. A Prompt step gets its own prompt and whatever is wired into it — never the article’s text. So the first step that has to understand the article is a Template step, and Prompt steps work on what earlier steps found. Most of the surprises on this page come back to this one rule.

2. Your first workflow

The builder opens in simple mode: pick up to six templates, and they all read the same article and get combined. That is the whole model. It covers most of what people want, and it is the fastest way to something useful.

Two templates over one article

whole workflow

The smallest workflow worth building: two perspectives on the same page, combined into one answer.

{
    "id": "chain_0000000000000001",
    "name": "Two-template starter",
    "description": "What it means for me, checked against what the article is not saying.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_001_personal_impact_lens",
            "stepPrompt": "What does this actually change for me? Be concrete.",
            "stepName": "Personal impact",
            "id": "step_0000000000000011"
        },
        {
            "type": "template",
            "templateId": "market_128_what_are_they_not_saying",
            "stepPrompt": "What is missing, hedged, or quietly assumed here?",
            "stepName": "What is missing",
            "id": "step_0000000000000012"
        }
    ],
    "finalPrompt": "Give me the personal consequence in three lines, then the single biggest gap in the reporting.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Neither step is wired to the other, so both read the article directly and run at the same time.
  • Five AI calls in total. A focus line on a Template step is a second call that rewrites the template's answer, so each step here costs two, plus one for the final answer.

Six templates — the simple-mode ceiling

whole workflow

What 'add up to six' costs: six parallel readings plus the synthesis.

{
    "id": "chain_0000000000000002",
    "name": "Full sweep",
    "description": "Six angles on one story, combined into a single brief.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Personal",
            "id": "step_0000000000000021"
        },
        {
            "type": "template",
            "templateId": "market_003_investment_insight_lens",
            "stepPrompt": "What changes for my holdings?",
            "stepName": "Investment",
            "id": "step_0000000000000022"
        },
        {
            "type": "template",
            "templateId": "market_007_policy_impact_lens",
            "stepPrompt": "What changes in policy terms?",
            "stepName": "Policy",
            "id": "step_0000000000000023"
        },
        {
            "type": "template",
            "templateId": "market_023_critical_analysis_lens",
            "stepPrompt": "Where is the argument weakest?",
            "stepName": "Critical",
            "id": "step_0000000000000024"
        },
        {
            "type": "template",
            "templateId": "market_104_second_order_effects_lens",
            "stepPrompt": "What follows from this that nobody mentions?",
            "stepName": "Second order",
            "id": "step_0000000000000025"
        },
        {
            "type": "template",
            "templateId": "market_126_what_happens_next",
            "stepPrompt": "What is the most likely next development?",
            "stepName": "Next",
            "id": "step_0000000000000026"
        }
    ],
    "finalPrompt": "Combine the six readings. Lead with the one that actually matters, and say plainly where they disagree.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Every step runs in parallel, so this is not six times slower than one template — but it is six times the cost. With a focus on each, that is twelve calls plus the final answer.
  • The builder shows a running AI-call estimate as you add steps. Watch it.

Switching to the advanced builder unlocks the other four step types, wiring between steps, per-step models, conditions, loops and publishing. Nothing is lost in the move: a simple workflow is a normal workflow, and simple mode is a view of it rather than a different format.

3. The final answer

The final answer is a model call like any other, and it is the one most worth spending a good model on — it is what you read. By default it receives the leaves of the workflow: the steps whose output no other step reads through InData or add-to. That is usually right, because it means the final answer sees conclusions rather than working notes.

One consequence worth knowing: a step that only decides whether another step runs, or only supplies the list another step repeats over, is still a leaf. Its answer reaches the final answer unless you choose the inputs by hand.

What the final answer reads by default

whole workflow

With nothing chosen, the final answer reads the steps that feed nothing downstream — the leaves.

{
    "id": "chain_0000000000000004",
    "name": "Draft then edit",
    "description": "A first pass that is rewritten by a second, so only the rewrite should reach the final answer.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_038_editor_in_chief",
            "stepPrompt": "Judge the news value honestly.",
            "stepName": "News value",
            "id": "step_0000000000000041",
            "addToStepIds": [
                "step_0000000000000042"
            ]
        },
        {
            "type": "template",
            "templateId": "market_070_headline_hunter_lens",
            "stepPrompt": "Write headline options for the strongest angle above.",
            "stepName": "Headlines",
            "id": "step_0000000000000042"
        }
    ],
    "finalPrompt": "Give me the five best headlines and one line on why the story is worth running.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • 'News value' feeds 'Headlines', so 'Headlines' is the only leaf and the only thing the final answer sees.
  • This is usually what you want: the final answer reads conclusions, not working notes.

Choosing the final answer's inputs by hand

whole workflow

finalInputSourceStepIds overrides the leaf default, here to put a step that feeds another in front of the final answer as well.

{
    "id": "chain_0000000000000005",
    "name": "Draft, edit, and keep the draft",
    "description": "The same draft-then-edit pair, but the final answer is shown the first pass too.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_038_editor_in_chief",
            "stepPrompt": "Judge the news value honestly.",
            "stepName": "News value",
            "id": "step_0000000000000051",
            "addToStepIds": [
                "step_0000000000000052"
            ]
        },
        {
            "type": "template",
            "templateId": "market_070_headline_hunter_lens",
            "stepPrompt": "Write headline options for the strongest angle above.",
            "stepName": "Headlines",
            "id": "step_0000000000000052"
        }
    ],
    "finalPrompt": "Give me the five best headlines, then the editor's verdict on news value in one line.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputSourceStepIds": [
        "step_0000000000000051",
        "step_0000000000000052"
    ]
}
  • Without the list, only 'Headlines' — the leaf — would reach the final answer. Naming 'News value' as well is the control for "I want this step's answer in the final answer too"; do not make a later step repeat it.
  • Every step must reach something. A step that feeds no other step and is left off a hand-picked list is refused when you save, because it would run and cost money for nothing.
  • An empty list is refused too, unless the article is re-attached — then the final answer reads the article and no steps.

Putting the original article back in front of the final answer

whole workflow

finalInputIncludesSeed re-attaches the source the steps were reading.

{
    "id": "chain_0000000000000006",
    "name": "Quote-checked brief",
    "description": "The final answer can quote the article because it can still see it.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_023_critical_analysis_lens",
            "stepPrompt": "Where does the argument break?",
            "stepName": "Critique",
            "id": "step_0000000000000061"
        }
    ],
    "finalPrompt": "Summarise the critique, and support each point with a short direct quotation from the article.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputIncludesSeed": true
}
  • Without this, the final answer sees only step outputs, so it cannot quote accurately and will tend to invent quotes.
  • Re-attaching the article does not change which steps are read: with no list chosen, the final answer still reads the leaves, and now the article as well.

Writing your own final instructions

whole workflow

Replacing the canned synthesis presets with your own final prompt.

{
    "id": "chain_0000000000000003",
    "name": "Action-only brief",
    "description": "Two readings, and a final answer that refuses to summarise.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Impact",
            "id": "step_0000000000000031"
        },
        {
            "type": "template",
            "templateId": "market_100_action_checklist_lens",
            "stepPrompt": "What should I actually do?",
            "stepName": "Actions",
            "id": "step_0000000000000032"
        }
    ],
    "finalPrompt": "Do not summarise the article. Output only: three actions I can take this week, each with the reason it is worth doing, and one thing I should deliberately not do.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Only the final prompt changes here. The presets in the builder write one for you; this replaces it with instructions of your own.

4. The five step types

Five types, in two families. The split is structural rather than cosmetic, and almost every rule further down this page turns on it.

In the builder In JSON Family Calls a model? What it does
TemplatetemplateProcessingYes Runs a template, optionally narrowed by a focus line. The only step that reads the article.
PromptpromptProcessingYes Runs your own system and user prompt over what is wired into it. Never sees the article.
Sub-workflowworkflowProcessingVia the child Runs another whole workflow and uses its final answer.
URLdataInputNo Fetches a page, or SEC filing data through the API.
ReferencetextInputNo Carries fixed text: a rulebook, a statute, a style guide.
ProfileprofileInputNo Hands on the reader's own profile, limited to the parts you tick.

Input steps supply data; they do not transform it. A URL, Reference or Profile step:

  • takes no InData — there is nothing upstream for it to read;
  • can never be the target of an "add to";
  • has no model, no output shape, no condition and no repeat.

Those four fields are not rejected if you send them — they are dropped when the workflow is saved. So a URL step you tried to put a loop on will run exactly once, quietly. This trips people up often enough to be worth stating twice.

Template step

one step

A template with a focus line that narrows a broad template to this workflow's job.

{
    "type": "template",
    "templateId": "market_003_investment_insight_lens",
    "stepPrompt": "Ignore the macro commentary. Only address what this does to the companies I hold.",
    "stepName": "Holdings only",
    "id": "step_0000000000000071"
}
  • The template supplies the perspective; stepPrompt (the builder calls it the focus) narrows it.
  • A focus is a second model call that rewrites the template's answer, so it is not free. Leave stepPrompt empty and the template's own answer is used as it is.
  • A Template step with nothing wired into it reads the article. It is the only step type that does.

Prompt step

one step

A raw system/user prompt pair that works on whatever is wired into it, when no existing template fits.

{
    "type": "prompt",
    "systemPrompt": "You are a precise extractor. You never infer, and you never fill gaps.",
    "userPrompt": "List every number in the text below with what it measures and the period it covers. If a number has no stated period, say so.",
    "stepName": "Extract numbers",
    "id": "step_0000000000000072"
}
  • A Prompt step never sees the article. It gets its own prompt and whatever is wired into it, so wire a Template, URL or Reference step in to give it something to work on.
  • Leave systemPrompt empty and the platform's default step system prompt is used instead.

URL step

one step

An input step that fetches a page and hands its content to whatever reads it.

{
    "type": "data",
    "provider": "url",
    "dataset": "page_content",
    "query": "https://www.sec.gov/news/pressreleases",
    "stepName": "Newsroom",
    "id": "step_0000000000000073"
}
  • No model call and no cost — it is a fetch.
  • An input step takes no InData. It produces input; it does not transform any.

Reference step

one step

A rulebook carried inside the workflow. No model call, no fetch, no failure mode.

{
    "type": "text",
    "body": "House style:\r\n1. No exclamation marks.\r\n2. Numbers under ten are spelled out.\r\n3. Never describe a fall as a 'crash' unless it exceeds 20 per cent.\r\n4. Attribute every forecast to the person who made it.",
    "sourceName": "styleguide.md",
    "stepName": "House style",
    "id": "step_0000000000000074"
}
  • The body is used literally. {$Step name} tokens inside it are NOT resolved — this is the single most common surprise.
  • Capped at 50 000 characters per step and 200 000 across the workflow.
  • Publishing the workflow publishes this text. Do not put anything private in a Reference step you intend to share.

Profile step

one step

The reader's own profile as text, for the prompt steps and the final prompt that otherwise never see it.

{
    "type": "profile",
    "profileReadAccess": [
        "about_you",
        "work_focus_areas"
    ],
    "stepName": "Our side",
    "id": "step_0000000000000076"
}
  • profileReadAccess takes the scopes a template declares. Leave it out, or empty, and the step reads nothing — unlike a template, where leaving it out means the whole profile.
  • The reader sees what the workflow reads before it runs, and can switch parts off for one run; the step then gets less, never more.
  • Fields the reader has not filled in are left out, and a step with nothing to show says so in a sentence, so a later prompt is not left to invent a reader.
  • Publishing the workflow publishes the step, not anybody's profile: each reader's run reads their own. In a Dropbox there is no reader profile, and the step says so.

Sub-workflow step

one step

Running another workflow and using its final answer as this step's output.

{
    "type": "workflow",
    "workflowId": "chain_000000000000005a",
    "stepName": "Standard due diligence",
    "id": "step_0000000000000075"
}
  • A live reference, not a copy: only the id is stored, so improving the child improves every parent.
  • The cost of that is a parent can break without changing. A child that is deleted, private to whoever runs it, cyclic, too deep or too large becomes a marker in the output rather than failing the run.

A Reference step is the cheap one: no model call, no network call, no failure mode, so it costs whoever runs the workflow nothing. A rubric, a statute or a house style held in one is cheaper than restating it in every prompt, and it changes in one place. In the builder you paste the text in or import it from a PDF — the text is extracted once and stored on the step, so the PDF is never needed again at run time.

5. Wiring steps together

There are two ways to connect two steps, and they meet in the middle:

  • InData (inputSourceStepIds) — the downstream step names what it wants to read. Pull.
  • Add to (addToStepIds) — the upstream step names where its output should go. Push.

Both make the receiving step wait for the sending one, and pushed output arrives under an ## Added from <step name> heading so the receiving model can tell the pieces apart. They differ in one way that matters, below.

A Template step with nothing wired reads the article

whole workflow

The seed — the page, PDF, clipboard or typed text — reaches every Template step that has no InData source.

{
    "id": "chain_0000000000000008",
    "name": "Seed default",
    "description": "One step, no wiring, so it reads the article.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Impact",
            "id": "step_0000000000000081"
        }
    ],
    "finalPrompt": "Give me the answer in three lines.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • This is why a brand-new workflow works before you wire anything: the builder starts you with Template steps.

Choosing InData replaces the article. This is the single most common surprise in the builder. A Template step with nothing wired in reads the seed. The moment you give it an InData source, it reads that source instead — the article is gone. The symptom is a step answering confidently about something the article never said, because all it ever saw was a summary.

Add-to keeps it. Output pushed into a Template step with add-to is appended to what the step already reads, so a step with no InData gets the article and the pushed output. That is how you give one step both — the article and a rulebook, or the article and a list of claims to check.

And a step that only needs to wait for another needs neither. A condition already makes a step wait for the step it tests, so a gated Template step should carry the condition and no InData: then it runs after the screen and still reads the article.

InData can also be set to None (inputSourcesCleared): the step drops the seed without naming a replacement, and reads only what other steps add to it. A template step set to None that nothing adds to has nothing to read, and the run stops there instead of sending the template an empty page.

Choosing InData REPLACES the article

whole workflow

The most common surprise in the builder: once a step has an upstream source, it no longer sees the page.

{
    "id": "chain_0000000000000009",
    "name": "InData replaces",
    "description": "Step two reads step one's output — and only that.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_extractor",
            "stepPrompt": "Summarise the article in five bullet points.",
            "stepName": "Summary",
            "id": "step_0000000000000091"
        },
        {
            "type": "template",
            "templateId": "market_124_explain_without_bs",
            "stepPrompt": "Rewrite the bullets for a non-expert.",
            "stepName": "Plain English",
            "id": "step_0000000000000092",
            "inputSourceStepIds": [
                "step_0000000000000091"
            ]
        }
    ],
    "finalPrompt": "Return the plain-English version.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • 'Plain English' cannot see the article. If it needs both, push the summary in with add-to instead of pulling it with InData: add-to keeps the article and appends.
  • Symptom of getting this wrong: a step confidently answers about something the article never said, because it only saw a summary.

Push your output into another step

whole workflow

addToStepIds sends this step's output into the target, under an '## Added from ...' heading — and unlike InData, the target keeps the article.

{
    "id": "chain_000000000000000a",
    "name": "Add-to routing",
    "description": "Step one pushes into step two rather than step two pulling from step one.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_extractor",
            "stepPrompt": "List the claims made in the article.",
            "stepName": "Claims",
            "id": "step_00000000000000a1",
            "addToStepIds": [
                "step_00000000000000a2"
            ]
        },
        {
            "type": "template",
            "templateId": "market_source_check",
            "stepPrompt": "Check each claim listed below against the article and mark it supported or unsupported.",
            "stepName": "Check",
            "id": "step_00000000000000a2"
        }
    ],
    "finalPrompt": "Report the unsupported claims.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • 'Check' reads the article and the claims together. Had it pulled the claims with InData instead, it would have lost the article.
  • An input step (URL or Reference) can never be an addTo target — it produces input, it does not receive any.

Explicit sources replace, and an inbound add-to still appends

whole workflow

The case people get wrong: choosing InData drops the article, but anything pushed in with add-to still arrives on top of what was chosen.

{
    "id": "chain_000000000000000b",
    "name": "Replace and append",
    "description": "Step three picks its own source and is also pushed into.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_extractor",
            "stepPrompt": "Summarise the article.",
            "stepName": "Summary",
            "id": "step_00000000000000b1",
            "addToStepIds": [
                "step_00000000000000b3"
            ]
        },
        {
            "type": "template",
            "templateId": "market_102_risk_register_lens",
            "stepPrompt": "List the risks in the article.",
            "stepName": "Risks",
            "id": "step_00000000000000b2"
        },
        {
            "type": "template",
            "templateId": "market_023_critical_analysis_lens",
            "stepPrompt": "Weigh the risks, using the summary for context.",
            "stepName": "Weighing",
            "id": "step_00000000000000b3",
            "inputSourceStepIds": [
                "step_00000000000000b2"
            ]
        }
    ],
    "finalPrompt": "Give me the weighted risk list.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • 'Weighing' sees the Risks output (chosen) and the Summary output (pushed in) — but not the article.

Three readings into one synthesis

whole workflow

Fan-in: several steps feeding one, which is the shape most workflows end up being.

{
    "id": "chain_000000000000000c",
    "name": "Fan-in",
    "description": "Three independent readings, reconciled by a fourth step.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_106_red_team_lens",
            "stepPrompt": "Argue against this.",
            "stepName": "Red team",
            "id": "step_00000000000000c1"
        },
        {
            "type": "template",
            "templateId": "market_107_blue_team_lens",
            "stepPrompt": "Argue for this.",
            "stepName": "Blue team",
            "id": "step_00000000000000c2"
        },
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Is this signal or noise?",
            "stepName": "Signal check",
            "id": "step_00000000000000c3"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Three analysts disagree below. Say who is right, and what evidence would change your mind.",
            "stepName": "Adjudicate",
            "id": "step_00000000000000c4",
            "inputSourceStepIds": [
                "step_00000000000000c1",
                "step_00000000000000c2",
                "step_00000000000000c3"
            ]
        }
    ],
    "finalPrompt": "Return the adjudication.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • The first three run at the same time. 'Adjudicate' waits for all three because it depends on all three.

One reading into three treatments

whole workflow

Fan-out: one expensive extraction reused by several cheap steps, instead of three steps re-reading the article.

{
    "id": "chain_000000000000000d",
    "name": "Fan-out",
    "description": "Extract once, then write three things from the extraction.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_077_fact_box_lens",
            "stepPrompt": "Extract the facts, figures and named parties.",
            "stepName": "Facts",
            "id": "step_00000000000000d1",
            "addToStepIds": [
                "step_00000000000000d2",
                "step_00000000000000d3",
                "step_00000000000000d4"
            ]
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Write a 40-word news-in-brief from the facts below.",
            "stepName": "Brief",
            "id": "step_00000000000000d2"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Write three headline options from the facts below.",
            "stepName": "Headlines",
            "id": "step_00000000000000d3"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Write a social post from the facts below.",
            "stepName": "Social",
            "id": "step_00000000000000d4"
        }
    ],
    "finalPrompt": "Return the brief, the headlines and the social post.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • One addToStepIds list can name several targets, which is the cheapest way to express fan-out.
  • The three writers are Prompt steps, so they see the facts and nothing else. That is the point: they write from one agreed set of facts.

Hold a step back to a later batch

whole workflow

startImmediately:false holds a step back until nothing else is ready and nothing is in flight.

{
    "id": "chain_000000000000000e",
    "name": "Quick reads first",
    "description": "Two quick readings come back first; the slow one waits its turn instead of competing with them.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Impact",
            "id": "step_00000000000000e1"
        },
        {
            "type": "template",
            "templateId": "market_023_critical_analysis_lens",
            "stepPrompt": "Where is this weak?",
            "stepName": "Critique",
            "id": "step_00000000000000e2"
        },
        {
            "type": "template",
            "templateId": "market_105_pre_mortem_lens",
            "stepPrompt": "Assume this goes badly in a year. Work through why, in detail.",
            "stepName": "Pre-mortem",
            "id": "step_00000000000000e3",
            "startImmediately": false
        }
    ],
    "finalPrompt": "Give the impact and the weak points first, then the pre-mortem.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • It is a scheduling hint, not a dependency. 'Pre-mortem' runs after the others, but it still reads only what it would have read anyway — here the article — and sees nothing the other steps produced.
  • To have a step read what the others wrote, wire them into it. Wiring already makes it wait, so it needs no delay as well.

A Prompt step never sees the article's text

whole workflow

Wire nothing in and it gets your prompt alone. Wire something in and it gets the page's title and address, your prompt and what was wired in — but still not the article.

{
    "id": "chain_000000000000000f",
    "name": "Preamble or not",
    "description": "Two prompt steps that differ only in whether anything is wired into them.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Write a haiku about deadlines.",
            "stepName": "Unwired",
            "id": "step_00000000000000f1"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Summarise what you were given.",
            "stepName": "Wired",
            "id": "step_00000000000000f2",
            "inputSourceStepIds": [
                "step_00000000000000f1"
            ]
        }
    ],
    "finalPrompt": "Return both.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • A model only reads the article through a Template step. Put one upstream of a Prompt step when the prompt needs the article's content.

6. Referring to other steps in a prompt

Wiring decides what a step receives. Tokens decide where that content lands in the prompt. There are four ways to name a step, and they are interchangeable:

TokenRefers toUse it when
{$Step name} A step by name, case-insensitively. Names are unique and stable. The most readable form.
{$step_…} A step by its stable id. The name might change, or is not unique.
{$Step.field} One declared field of a step’s output shape. You want a value, not a whole answer.
{$step_number_4} A step by position. Quick to type. The builder keeps the number pointing at the same step when you reorder. Avoid it past a repeated step or a sub-workflow, whose expansion shifts the numbers at run time.

Reference a step by its name

whole workflow

{$Step name} pulls that step's whole output into a prompt. The name must be unique in the workflow.

{
    "id": "chain_0000000000000010",
    "name": "Token by name",
    "description": "The final answer names the step it wants.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_102_risk_register_lens",
            "stepPrompt": "List the risks in the article.",
            "stepName": "Risks",
            "id": "step_0000000000000101"
        },
        {
            "type": "template",
            "templateId": "market_103_opportunity_brief_lens",
            "stepPrompt": "List the opportunities in the article.",
            "stepName": "Opportunities",
            "id": "step_0000000000000102"
        }
    ],
    "finalPrompt": "Weigh these against each other.\r\n\r\nRISKS:\r\n{$Risks}\r\n\r\nOPPORTUNITIES:\r\n{$Opportunities}",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Name matching is case-insensitive.
  • A token in the final prompt always resolves, because the final answer runs after every step. In a step's own prompt, the step it names has to be wired upstream — see the next callout.
  • If two steps share a name the token cannot be resolved to one of them, so keep names unique.

Reference a step by its id

whole workflow

{$step_...} survives renaming. Verbose, but it never breaks.

{
    "id": "chain_0000000000000011",
    "name": "Token by id",
    "description": "The same reference, pinned to the step's stable id.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_102_risk_register_lens",
            "stepPrompt": "List the risks in the article.",
            "stepName": "Risks",
            "id": "step_0000000000000111"
        }
    ],
    "finalPrompt": "Summarise this risk list in one paragraph:\r\n\r\n{$step_0000000000000111}",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Use this when a step's name is likely to change, or when a name is not unique.

Reference a step by its position

whole workflow

{$step_number_2} means 'step 2'. The builder rewrites the number when you reorder, so it follows the same step.

{
    "id": "chain_0000000000000012",
    "name": "Token by number",
    "description": "Positional reference, which the builder keeps pointing at the step you meant.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_extractor",
            "stepPrompt": "List the claims.",
            "stepName": "Claims",
            "id": "step_0000000000000121",
            "addToStepIds": [
                "step_0000000000000122"
            ]
        },
        {
            "type": "template",
            "templateId": "market_source_check",
            "stepPrompt": "Check each claim listed below against the article.",
            "stepName": "Check",
            "id": "step_0000000000000122"
        }
    ],
    "finalPrompt": "Report on the check:\r\n\r\n{$step_number_2}",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Step-number references are sticky: insert a step above and the number updates to keep following the same step.
  • That stickiness only applies inside the builder. A number you type into raw JSON is just a number.
  • Numbers count positions after repeats and sub-workflows are expanded at run time, so a number that points past a repeated step or a sub-workflow lands on a different step. Use a name or an id there.

Reference one field of a declared output shape

whole workflow

{$Step.field} reads a single named field instead of the step's whole answer.

{
    "id": "chain_0000000000000013",
    "name": "Token by field",
    "description": "The classifier declares fields; the next step reads one of them.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Classify the article and score how consequential it is.",
            "stepName": "Classify",
            "id": "step_0000000000000131",
            "outputSchema": {
                "fields": [
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion",
                            "advertorial"
                        ]
                    },
                    {
                        "name": "score",
                        "type": "number",
                        "description": "0-10, how consequential"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "It scored {$Classify.score} out of 10 and is a {$Classify.kind}. Write a one-line editor's note saying whether to run it.",
            "stepName": "Editor note",
            "id": "step_0000000000000132",
            "inputSourceStepIds": [
                "step_0000000000000131"
            ]
        }
    ],
    "finalPrompt": "Return the editor's note.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • A dotted token only resolves if the source step actually declared that field in its output shape.
  • 'Editor note' names Classify in InData as well as in its prompt. The InData is what makes it wait for Classify; the token only decides where the value lands.
  • {$Classify} on its own still works, and gives the declared fields as 'name: value' lines.

Name, id and number in one prompt

whole workflow

The three reference forms are interchangeable and can be mixed freely.

{
    "id": "chain_0000000000000014",
    "name": "Mixed tokens",
    "description": "One final prompt using every reference form at once.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_077_fact_box_lens",
            "stepPrompt": "List the facts.",
            "stepName": "Facts",
            "id": "step_0000000000000141"
        },
        {
            "type": "template",
            "templateId": "market_102_risk_register_lens",
            "stepPrompt": "List the risks.",
            "stepName": "Risks",
            "id": "step_0000000000000142"
        },
        {
            "type": "template",
            "templateId": "market_100_action_checklist_lens",
            "stepPrompt": "List the actions.",
            "stepName": "Actions",
            "id": "step_0000000000000143"
        }
    ],
    "finalPrompt": "FACTS (by name):\r\n{$Facts}\r\n\r\nRISKS (by id):\r\n{$step_0000000000000142}\r\n\r\nACTIONS (by number):\r\n{$step_number_3}\r\n\r\nNow reconcile them.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Mixing forms is legal. Picking one and sticking to it is kinder to whoever edits the workflow next.

A token does not make a step wait. Only InData, add-to, a condition or a repeat does. A token in a step’s prompt naming a step that is not wired upstream is usually filled in while that step is still running — and a step that has not answered yet resolves to nothing. So wire every step you name in a prompt. The final prompt is the exception: it runs after every step, so its tokens always resolve.

A token inside a URL step's address

whole workflow

A URL step's query is templated too, so one step can decide what the next one fetches — provided the fetch is held back until that step has answered.

{
    "id": "chain_0000000000000015",
    "name": "Computed fetch",
    "description": "The model picks a URL; the URL step fetches it.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_extractor",
            "stepPrompt": "Find the investor-relations page of the company the article is about.",
            "stepName": "Find IR page",
            "id": "step_0000000000000151",
            "outputSchema": {
                "fields": [
                    {
                        "name": "url",
                        "type": "string",
                        "description": "The canonical https URL, nothing else"
                    }
                ]
            }
        },
        {
            "type": "data",
            "provider": "url",
            "dataset": "page_content",
            "query": "{$Find IR page.url}",
            "stepName": "Fetch IR page",
            "id": "step_0000000000000152",
            "startImmediately": false,
            "addToStepIds": [
                "step_0000000000000153"
            ]
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Summarise the investor-relations page below: results date, guidance, and anything flagged as material.",
            "stepName": "IR summary",
            "id": "step_0000000000000153"
        }
    ],
    "finalPrompt": "Return the investor-relations summary.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • A URL step cannot be wired to anything upstream, so nothing makes it wait. Left alone it would run at once, before 'Find IR page' answered, and fetch an empty address.
  • startImmediately:false is what fixes that: the fetch is offered only once nothing else is ready or running — by which time the address is in.
  • The address comes from a declared field rather than the step's prose, so no stray sentence reaches the fetch.

Tokens do not work in a Reference step. Its body is used literally — a Reference step is an input, so it takes nothing from upstream — and the token reaches the model exactly as you typed it.

Tokens do NOT work in a Reference step

one step

The counter-example worth remembering: a Reference body is used literally, tokens and all.

{
    "type": "text",
    "body": "Compare the draft against these rules. Do not attempt to resolve {$Draft} here — this line will reach the model exactly as written.",
    "stepName": "Rules",
    "id": "step_0000000000000158"
}
  • A Reference step is an input: it takes nothing from upstream, runs before everything that reads it, and is used exactly as written.
  • If you need upstream output mixed with fixed rules, put the rules in the Reference step and do the mixing in a prompt step that reads both.

If a token names nothing it can resolve — a step that does not exist, or a name two steps share — a plain one such as {$Missing} resolves to nothing and disappears. One containing anything other than letters, digits, underscores and dots (a space, a hyphen, an accented letter) is left in the prompt verbatim, on the assumption it was prose rather than a reference.

7. Asking a step for named fields

By default a step answers in prose, and everything downstream gets that prose. An output shape additionally asks the step for named, typed fields. That is what unlocks the three things on the rest of this page: {$Step.field}, conditions on a field, and repeating over a list.

A shaped step answers with its fields and nothing else. Downstream steps, the final answer and {$Step name} all get those fields as name: value lines, not prose. So declare a shape on a step whose job is to decide or extract, and leave it off a step whose writing you want to read.

TypeHoldsNotes
stringFree text.The default choice.
numberA number.The only type the numeric comparisons can use.
booleanYes or no.Pairs with the isTrue comparison.
enumOne of a fixed list.Must carry its values. An enum with no values is not one.
string[]A list of strings.The only type a repeat can iterate over.

Up to 20 fields per step. Field names start with a letter, may contain lower-case letters, digits and underscores, and are at most 40 characters. An enum may list up to 20 values of 80 characters each.

On a Template step a shape costs a second call, the same way a focus does: the template reads the article, then a short pass fills in the fields from its answer. Both calls use the step’s model.

Asking a step for named fields

one step

An output shape turns a step's answer into fields you can test, read individually, and loop over.

{
    "type": "template",
    "templateId": "market_131_signal_or_noise",
    "stepPrompt": "Classify this article and score it.",
    "stepName": "Screen",
    "id": "step_0000000000000161",
    "outputSchema": {
        "fields": [
            {
                "name": "kind",
                "type": "enum",
                "values": [
                    "news",
                    "opinion",
                    "analysis"
                ]
            },
            {
                "name": "score",
                "type": "number",
                "description": "0-10, how consequential"
            }
        ]
    }
}
  • Declaring a shape is what unlocks {$Step.field}, conditions on a field, and looping.
  • The step then answers with the fields and nothing else. Downstream steps and {$Screen} get them as 'kind: news' lines rather than prose.
  • On a Template step a shape costs a second call, like a focus does: the template reads the article, then a short pass fills in the fields. Both use the step's model.

Reading one field downstream

whole workflow

Once declared, a field is addressable on its own.

{
    "id": "chain_0000000000000017",
    "name": "Read one field",
    "description": "A score declared in one step and used in the next.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Score how consequential the article is.",
            "stepName": "Screen",
            "id": "step_0000000000000171",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "The desk scored this {$Screen.score}/10. Write the cover note.",
            "stepName": "Cover note",
            "id": "step_0000000000000172",
            "inputSourceStepIds": [
                "step_0000000000000171"
            ]
        }
    ],
    "finalPrompt": "Return the cover note.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Array fields are joined with commas when read this way.

A list field, the prerequisite for looping

one step

Only a declared string[] field can drive 'repeat per item'.

{
    "type": "template",
    "templateId": "market_exposure_mapper",
    "stepPrompt": "Identify every company materially affected by this story.",
    "stepName": "Find companies",
    "id": "step_0000000000000181",
    "outputSchema": {
        "fields": [
            {
                "name": "tickers",
                "type": "string[]",
                "description": "One ticker per affected company"
            }
        ]
    }
}
  • How many model calls the run makes comes out of this field, which is exactly why it has to be declared rather than parsed out of prose.

All five field types at once

one step

string, number, boolean, enum and string[] — the complete set.

{
    "type": "template",
    "templateId": "market_131_signal_or_noise",
    "stepPrompt": "Assess this article.",
    "stepName": "Assess",
    "id": "step_0000000000000191",
    "outputSchema": {
        "fields": [
            {
                "name": "headline",
                "type": "string",
                "description": "One-line summary"
            },
            {
                "name": "score",
                "type": "number",
                "description": "0-10"
            },
            {
                "name": "is_paywalled",
                "type": "boolean"
            },
            {
                "name": "kind",
                "type": "enum",
                "values": [
                    "news",
                    "opinion",
                    "analysis",
                    "advertorial"
                ]
            },
            {
                "name": "entities",
                "type": "string[]",
                "description": "Named organisations"
            }
        ]
    }
}
  • Field names start with a letter and allow lower-case letters, digits and underscores — up to 40 characters. Upper case is lowered for you.
  • An enum must carry its values. An enum with no values is not one.
  • Up to 20 fields per step.

When a model ignores the shape the step is retried once, with a correction prompt. If the second attempt is also unparseable the run fails rather than guessing. One retry rather than several is deliberate: whoever runs the workflow pays for every attempt.

8. Conditions: running a step only sometimes

A step can carry one condition. If it does not hold, the step is skipped before any model is called — so a skipped step costs nothing. This is the main lever for keeping a workflow cheap: screen with a small model, and buy the expensive one only when the screen earns it.

A condition is a step, an optional field, one of nine comparisons, and a value. It is deliberately not an expression language: there is nothing to parse and nothing to sandbox, and it renders as three dropdowns and a box.

In the builderIn JSONTrue when
iseqThe value matches, trimmed and case-insensitively.
is notneThe value does not match.
containscontainsThe text contains the value, case-insensitively.
is greater thangtNumerically greater.
is at leastgteNumerically greater or equal.
is less thanltNumerically less.
is at mostlteNumerically less or equal.
has any valueexistsNot blank; for a list, not empty. Takes no value.
is yesisTrueThe value is true. Takes no value.

Three things worth knowing before you rely on one:

  • The four numeric comparisons are numeric only. If either side is not a number the answer is false rather than a coerced guess — that is what keeps "greater than" from quietly meaning "alphabetically after". For a list field, the comparison uses the number of items.
  • Naming no field compares against the step’s whole prose answer. Convenient, and the least reliable option here: contains "lawsuit" also matches "no lawsuit has been filed".
  • If the step the condition points at was itself skipped, the condition is false — for ne as much as for eq. It never throws.

A condition cannot point at a repeated step, because there is no single answer to test; the save is refused. Give the gated step no InData unless it genuinely needs the screen’s answer as its input — the condition already makes it wait, and without InData it still reads the article.

Run only if a field equals a value

whole workflow

The workhorse condition: gate a branch on a classifier's answer.

{
    "id": "chain_000000000000001a",
    "name": "Opinion-only branch",
    "description": "The fact-check only runs on opinion pieces.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Classify the article.",
            "stepName": "Classify",
            "id": "step_00000000000001a1",
            "outputSchema": {
                "fields": [
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion",
                            "analysis"
                        ]
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_023_critical_analysis_lens",
            "stepPrompt": "Fact-check the argument.",
            "stepName": "Fact check",
            "id": "step_00000000000001a2",
            "runIf": {
                "step": "step_00000000000001a1",
                "field": "kind",
                "op": "eq",
                "value": "opinion"
            }
        }
    ],
    "finalPrompt": "Report the classification, and the fact-check if there was one.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • eq and ne compare as text, trimmed and case-insensitive, so 'Opinion' matches 'opinion'.
  • 'Fact check' has no InData. The condition alone makes it wait for Classify, and leaving InData empty keeps the article in front of it — wire Classify in and it would fact-check a one-word classification.
  • A skipped step costs nothing — the model is never called.

Run only if a number clears a threshold

whole workflow

The cheap-screen-then-expensive-analysis pattern, which is the main reason conditions exist.

{
    "id": "chain_000000000000001d",
    "name": "Threshold gate",
    "description": "A cheap model screens; an expensive one only runs when it is worth it.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Score how consequential the article is, 0-10.",
            "stepName": "Screen",
            "id": "step_00000000000001d1",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_104_second_order_effects_lens",
            "stepPrompt": "Work through the second- and third-order consequences in detail.",
            "stepName": "Deep dive",
            "id": "step_00000000000001d2",
            "runIf": {
                "step": "step_00000000000001d1",
                "field": "score",
                "op": "gte",
                "value": "7"
            }
        }
    ],
    "finalPrompt": "If there was a deep dive, lead with it. Otherwise say why this was not worth one.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • gt, gte, lt and lte are numeric only. If either side is not a number the condition answers false rather than guessing — that keeps 'greater than' from quietly meaning 'alphabetically after'.
  • For a list field, the comparison uses the number of items.
  • Always give a numeric comparison a value. An empty value is not a number, and the widget and the Dropbox runner disagree about what it means.

Run only if a boolean is yes

whole workflow

isTrue is unary: there is no value to fill in.

{
    "id": "chain_000000000000001b",
    "name": "Listed-company branch",
    "description": "Only read the market angle when the story actually names a listed company.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_exposure_mapper",
            "stepPrompt": "Does the article name a publicly listed company?",
            "stepName": "Listed check",
            "id": "step_00000000000001b1",
            "outputSchema": {
                "fields": [
                    {
                        "name": "names_listed_company",
                        "type": "boolean"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_003_investment_insight_lens",
            "stepPrompt": "What does this mean for the listed companies it names?",
            "stepName": "Market angle",
            "id": "step_00000000000001b2",
            "runIf": {
                "step": "step_00000000000001b1",
                "field": "names_listed_company",
                "op": "isTrue"
            }
        }
    ],
    "finalPrompt": "Give the market angle if there is one; otherwise say the story names no listed company.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • isTrue matches a real boolean true, or the text 'true'. Nothing else.

Run only if a field came back at all

one step

exists is the other unary op: it asks whether there is anything there.

{
    "type": "template",
    "templateId": "market_003_investment_insight_lens",
    "stepPrompt": "What does this mean for the companies it affects?",
    "stepName": "Company impact",
    "id": "step_00000000000001c1",
    "runIf": {
        "step": "step_0000000000000181",
        "field": "tickers",
        "op": "exists"
    }
}
  • For a list, exists means 'not empty'. For anything else it means 'not blank'.
  • Here it reads the 'Find companies' step from the list-field example above: no company named, no impact reading.

Everything except one case

one step

ne inverts eq, which is usually clearer than listing the cases you do want.

{
    "type": "template",
    "templateId": "market_100_action_checklist_lens",
    "stepPrompt": "What should I do about this?",
    "stepName": "Actions",
    "id": "step_00000000000001f1",
    "runIf": {
        "step": "step_00000000000001a1",
        "field": "kind",
        "op": "ne",
        "value": "advertorial"
    }
}
  • Reach for ne when the exception list is shorter than the inclusion list.

Searching a step's prose with no field named

one step

Leave the field blank and 'contains' searches the whole answer. Convenient, and the least reliable op here.

{
    "type": "template",
    "templateId": "market_008_legal_insight_lens",
    "stepPrompt": "Draft the legal escalation note.",
    "stepName": "Escalate",
    "id": "step_00000000000001e1",
    "runIf": {
        "step": "step_00000000000001d1",
        "op": "contains",
        "value": "lawsuit"
    }
}
  • This matches the word anywhere in the step's text, including inside 'no lawsuit has been filed'.
  • Prefer a declared boolean or enum field. Use prose search only when you cannot change the upstream step.

There is no AND, and no OR

One condition per step, and no nesting. The simplest way round it is usually to ask the screen the combined question as one field — a boolean that is true for a high-scoring opinion piece — and gate on that. When the tests genuinely belong to different steps, split them across two, as below.

Two conditions (AND), expressed as a workflow

whole workflow

There is no AND. Put the gates in sequence and point the second condition at the first gate: a condition on a skipped step is false, so B runs only if A ran.

{
    "id": "chain_0000000000000020",
    "name": "Gates in sequence",
    "description": "High score AND opinion — as two steps, each carrying one condition.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Classify and score the article.",
            "stepName": "Screen",
            "id": "step_0000000000000201",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    },
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion"
                        ]
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Repeat the kind given below, exactly.",
            "stepName": "Gate 1",
            "id": "step_0000000000000202",
            "modelId": "gpt-4o-mini",
            "inputSourceStepIds": [
                "step_0000000000000201"
            ],
            "outputSchema": {
                "fields": [
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion"
                        ]
                    }
                ]
            },
            "runIf": {
                "step": "step_0000000000000201",
                "field": "score",
                "op": "gte",
                "value": "7"
            }
        },
        {
            "type": "template",
            "templateId": "market_023_critical_analysis_lens",
            "stepPrompt": "Full critical review.",
            "stepName": "Review",
            "id": "step_0000000000000203",
            "runIf": {
                "step": "step_0000000000000202",
                "field": "kind",
                "op": "eq",
                "value": "opinion"
            }
        }
    ],
    "finalPrompt": "Report the review if there was one.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputSourceStepIds": [
        "step_0000000000000203"
    ]
}
  • 'Review' tests Gate 1, not Screen. When the score is under 7, Gate 1 is skipped, and a condition on a skipped step is false — so Review is skipped whatever the kind. That is the AND.
  • Pointing Review's condition at Screen instead would not work: Review would still depend on a step that ran, and would run on every opinion piece whatever its score.
  • Often simpler still: ask the screen for the combined answer as one field — a boolean worth_review that is true for a high-scoring opinion piece — and gate on that. One condition, no extra step.

Two conditions (OR), expressed as siblings

whole workflow

There is no OR either. Two sibling branches feeding one step gives you OR, because a skipped branch simply contributes nothing.

{
    "id": "chain_0000000000000021",
    "name": "Sibling gates",
    "description": "Either branch may fire; the synthesis takes whatever arrived.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Classify and score the article.",
            "stepName": "Screen",
            "id": "step_0000000000000211",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    },
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion"
                        ]
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_104_second_order_effects_lens",
            "stepPrompt": "Explain why this matters so much.",
            "stepName": "High score branch",
            "id": "step_0000000000000212",
            "addToStepIds": [
                "step_0000000000000214"
            ],
            "runIf": {
                "step": "step_0000000000000211",
                "field": "score",
                "op": "gte",
                "value": "8"
            }
        },
        {
            "type": "template",
            "templateId": "market_023_critical_analysis_lens",
            "stepPrompt": "Explain the argument being made.",
            "stepName": "Opinion branch",
            "id": "step_0000000000000213",
            "addToStepIds": [
                "step_0000000000000214"
            ],
            "runIf": {
                "step": "step_0000000000000211",
                "field": "kind",
                "op": "eq",
                "value": "opinion"
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Write the desk note from whatever is below.",
            "stepName": "Desk note",
            "id": "step_0000000000000214"
        }
    ],
    "finalPrompt": "Return the desk note, or say the story did not qualify.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputSourceStepIds": [
        "step_0000000000000214"
    ]
}
  • 'Desk note' has no condition, so it runs if either branch reached it. That is the OR.
  • When neither branch fires, every input it has was skipped, so it is skipped too — and the final answer is written from nothing but its own prompt. Say what to do in that case, as this one does.
  • The final inputs are chosen by hand because Screen is only a gate here. Left to the default, Screen is a leaf — it feeds no step through InData or add-to — and its scores would reach the final answer.

Both patterns rest on two rules. A condition on a skipped step is false. And skips travel downstream: a step every one of whose inputs was skipped is skipped too, and a skipped branch never reaches the final answer. “Every one” is the catch — a step that also depends on something that ran, including the step its own condition reads, still runs. The run trace names the reason for each skip: condition-false for the step that failed its own test, upstream-skipped for everything that depended on it, and for-each-empty for a repeat with nothing to repeat over.

Skips travel downstream

whole workflow

A step whose every input was skipped is skipped too, and a skipped branch never reaches the final answer.

{
    "id": "chain_0000000000000022",
    "name": "Skip cascade",
    "description": "One false condition takes the whole branch with it.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Score how consequential the article is.",
            "stepName": "Screen",
            "id": "step_0000000000000221",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_104_second_order_effects_lens",
            "stepPrompt": "Deep analysis.",
            "stepName": "Analysis",
            "id": "step_0000000000000222",
            "runIf": {
                "step": "step_0000000000000221",
                "field": "score",
                "op": "gte",
                "value": "9"
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Write up the analysis below.",
            "stepName": "Write-up",
            "id": "step_0000000000000223",
            "inputSourceStepIds": [
                "step_0000000000000222"
            ]
        }
    ],
    "finalPrompt": "Report whatever survived.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputSourceStepIds": [
        "step_0000000000000223"
    ]
}
  • 'Write-up' carries no condition of its own, but it is skipped because its only input was.
  • The run trace names the reason: condition-false for the first, upstream-skipped for the second.
  • Only steps that feed nothing else count as inputs to that rule's "all": a step that is also wired to something that ran still runs.

9. Repeating a step once per item

Have an upstream step declare a string[] field, then set a step to repeat over it. You get one copy of that step per item, each with these tokens filled in:

  • {$item} — the current item.
  • {$index} — its position, counting from 1.
  • {$count} — how many copies run (after the cap below).

They are substituted into the system prompt, the user prompt and a template focus. Each copy is named after its item — Dive (AAPL), Dive (MSFT) — so name the step for what it does and leave {$item} out of the name, where it would stay literally.

There is no loop edge and no collector step. The step is cloned into N steps before the workflow is planned, and everything that referenced the original now references every copy. That is why the flow chart shows parallel nodes rather than a loop arrow, and why the step that consumed the original needs no changes at all — it simply receives N inputs instead of one.

Repeat a step once per item

whole workflow

forEach fans a step out into one copy per item in an upstream list field.

{
    "id": "chain_0000000000000023",
    "name": "Per-company deep dive",
    "description": "One analysis per affected company, from a list the previous step declared.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_exposure_mapper",
            "stepPrompt": "Identify every company materially affected by the article.",
            "stepName": "Find companies",
            "id": "step_0000000000000231",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_003_investment_insight_lens",
            "stepPrompt": "Company {$index} of {$count}: assess the impact on {$item} specifically.",
            "stepName": "Dive",
            "id": "step_0000000000000232",
            "inputSourceStepIds": [
                "step_0000000000000231"
            ],
            "forEach": {
                "step": "step_0000000000000231",
                "field": "tickers"
            }
        }
    ],
    "finalPrompt": "One paragraph per company, then the single most exposed name.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • {$item} is the current item, {$index} counts from 1, and {$count} is how many copies run.
  • Those three tokens are substituted into the system prompt, the user prompt and the focus.
  • Each copy is named after its item — 'Dive (AAPL)', 'Dive (MSFT)' — so the flow chart and the combined output can tell them apart.
  • Each copy is a separate paid run of the step. Twelve tickers means twelve copies, and with a focus each copy is two calls.
  • Capped at 20 copies per step and 100 across a run. Past that the run reports 'ran 20 of 63' rather than failing.

Naming the copies

one step

Each copy is named after its item automatically. Name the step for what it does, and leave {$item} out of the name.

{
    "type": "template",
    "templateId": "market_003_investment_insight_lens",
    "stepPrompt": "Assess {$item}.",
    "stepName": "Dive",
    "id": "step_0000000000000241",
    "forEach": {
        "step": "step_0000000000000231",
        "field": "tickers"
    }
}
  • Copies are named 'Dive (AAPL)', 'Dive (MSFT)' and so on, from the step name and the first 40 characters of the item.
  • A {$item} typed into the name stays there literally: 'Dive: {$item}' runs as 'Dive: {$item} (AAPL)'.

Every copy reaches the step that consumed the original

whole workflow

There is no collector step. Whatever referenced the repeated step now receives each copy under its own heading.

{
    "id": "chain_0000000000000025",
    "name": "Loop then summarise",
    "description": "Many analyses, one summary, no plumbing.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_exposure_mapper",
            "stepPrompt": "List the companies the article affects.",
            "stepName": "Companies",
            "id": "step_0000000000000251",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Assess {$item}.",
            "stepName": "Assess",
            "id": "step_0000000000000252",
            "inputSourceStepIds": [
                "step_0000000000000251"
            ],
            "addToStepIds": [
                "step_0000000000000253"
            ],
            "forEach": {
                "step": "step_0000000000000251",
                "field": "tickers"
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Rank the assessments below by exposure.",
            "stepName": "Rank",
            "id": "step_0000000000000253"
        }
    ],
    "finalPrompt": "Return the ranking.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • The expansion happens before anything looks at the graph, so 'Rank' simply sees N inputs instead of one.
  • 'Assess' is a Prompt step reading the list through InData, so each copy works from the company name alone. Make it a Template step with no InData, as in the first example, when each copy should read the article.
  • That is also why there is no loop arrow in the flow chart: the chart shows N parallel nodes.

An empty list skips the step

whole workflow

If the list comes back empty the repeated step is skipped, not stuck.

{
    "id": "chain_0000000000000027",
    "name": "Empty list",
    "description": "Nothing to iterate over is a normal outcome, not an error.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_exposure_mapper",
            "stepPrompt": "List the companies the article affects, or return an empty list if none are.",
            "stepName": "Companies",
            "id": "step_0000000000000271",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Assess {$item}.",
            "stepName": "Assess",
            "id": "step_0000000000000272",
            "inputSourceStepIds": [
                "step_0000000000000271"
            ],
            "forEach": {
                "step": "step_0000000000000271",
                "field": "tickers"
            }
        }
    ],
    "finalPrompt": "If no companies were affected, say so plainly.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • The trace records the reason as for-each-empty, and the run continues.

Input steps cannot repeat

whole workflow

The limit worth knowing before you design around it: forEach belongs to steps that call a model, so a URL or Reference step can never be repeated.

{
    "id": "chain_0000000000000026",
    "name": "Loop what thinks, not what fetches",
    "description": "The list drives analysis steps; the fetching is done by ordinary URL steps.",
    "steps": [
        {
            "type": "data",
            "provider": "url",
            "dataset": "page_content",
            "query": "https://example.com/source-a",
            "stepName": "Source A",
            "id": "step_0000000000000261"
        },
        {
            "type": "data",
            "provider": "url",
            "dataset": "page_content",
            "query": "https://example.com/source-b",
            "stepName": "Source B",
            "id": "step_0000000000000262"
        },
        {
            "type": "template",
            "templateId": "market_extractor",
            "stepPrompt": "List the factual claims the article makes.",
            "stepName": "Claims",
            "id": "step_0000000000000263",
            "outputSchema": {
                "fields": [
                    {
                        "name": "claims",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Claim {$index} of {$count}: \"{$item}\". Is it supported by the sources below? Answer supported, contradicted or unaddressed, with the evidence.",
            "stepName": "Check",
            "id": "step_0000000000000264",
            "inputSourceStepIds": [
                "step_0000000000000261",
                "step_0000000000000262"
            ],
            "forEach": {
                "step": "step_0000000000000263",
                "field": "claims"
            }
        }
    ],
    "finalPrompt": "List the contradicted claims first, then the unaddressed ones.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • If you put forEach on a URL or Reference step it is silently dropped on save, and the step runs once.
  • So there is no way to fetch N addresses from one step. Add a URL step per source, or compute one address upstream as in the URL-token example above.
  • Here the loop is on the checking step, which is where the thinking happens anyway.

Repeats are capped at 20 per step and 100 per run. Past the cap the run does not fail — it runs what it can and reports how many, so a list of 63 becomes "ran 20 of 63". How many model calls a run makes comes straight out of that list field, which is exactly why the field has to be declared rather than parsed out of prose. Cap the list in the extracting step’s own prompt too, rather than relying on the limit.

10. Sub-workflows

A sub-workflow step runs another whole workflow and uses its final answer as this step’s output. Only the child’s id is stored and it is resolved when the parent runs, so improving the child improves every parent that uses it.

The price of that is a parent can break without ever being edited. Rather than fail the run, an unusable child degrades to a marker in the output. There are five ways that happens: the child was deleted, it is private to whoever runs the parent, including it would form a cycle, it would sit more than three levels deep, or inlining it would blow the step budget.

A sub-workflow step costs whatever the child costs, plus one call for the child’s own final answer, and counts as its steps plus one against the parent’s step budget. It cannot repeat and cannot pick a model; if the repetition belongs inside the child, put the repeat there.

Calling another workflow

whole workflow

A sub-workflow step runs a whole other workflow and uses its final answer as this step's output.

{
    "id": "chain_0000000000000028",
    "name": "Parent with a sub-workflow",
    "description": "Reusing a standard analysis instead of copying its steps.",
    "steps": [
        {
            "type": "workflow",
            "workflowId": "chain_000000000000005a",
            "stepName": "Standard analysis",
            "id": "step_0000000000000281"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Turn the analysis below into a one-page memo.",
            "stepName": "Memo",
            "id": "step_0000000000000282",
            "inputSourceStepIds": [
                "step_0000000000000281"
            ]
        }
    ],
    "finalPrompt": "Return the memo.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "runtimeWorkflows": [
        {
            "id": "chain_000000000000005a",
            "name": "Standard analysis",
            "steps": [
                {
                    "type": "template",
                    "templateId": "market_001_personal_impact_lens",
                    "stepPrompt": "What changes?",
                    "stepName": "Impact",
                    "id": "step_00000000000005a1"
                },
                {
                    "type": "template",
                    "templateId": "market_023_critical_analysis_lens",
                    "stepPrompt": "Where is it weak?",
                    "stepName": "Critique",
                    "id": "step_00000000000005a2"
                }
            ],
            "finalUserPrompt": "Combine the two readings."
        }
    ]
}
  • runtimeWorkflows carries the resolved child when the workflow is handed to the extension. In storage only the child's id is kept.
  • Nesting is capped at three levels deep, and the child's steps — plus one for its final answer — count against the parent's step budget.
  • Cost is the child's own calls plus one for its final answer.

When a sub-workflow cannot be resolved

whole workflow

A deleted, newly private, cyclic or too-large child degrades to a marker step instead of failing the run.

{
    "id": "chain_0000000000000029",
    "name": "Broken sub-workflow",
    "description": "The parent still produces an answer, and says what was missing.",
    "steps": [
        {
            "type": "workflow",
            "workflowId": "chain_000000000000005b",
            "stepName": "Missing child",
            "id": "step_0000000000000291"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Work with whatever arrived below.",
            "stepName": "Continue",
            "id": "step_0000000000000292",
            "inputSourceStepIds": [
                "step_0000000000000291"
            ]
        }
    ],
    "finalPrompt": "Answer, and note anything that was unavailable.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • The step's output becomes a marker reading [SUB-WORKFLOW UNAVAILABLE: ...] and the run carries on.
  • Five causes: the child was deleted, it is private to whoever runs the parent, it would form a cycle, it sits more than three levels deep, or inlining it would blow the step budget.
  • This is the price of a live reference. A copy could not break this way, but it also could not improve when the child does.

11. Models and what a workflow costs

Each step can pick its own model, or leave it blank to use whatever the widget is set to. The arithmetic: one call per processing step that actually runs, plus one for the final answer — with these adjustments:

  • A Template step with a focus or an output shape is two calls: the template, then the pass that applies the focus or fills in the fields.
  • A repeated step costs one run per copy.
  • A sub-workflow costs the child’s calls plus one for its final answer.
  • URL and Reference steps are free. Skipped steps are free.
  • An empty reply is retried once, and so is an answer that ignored its shape.

Because ready steps run in parallel, a twelve-step workflow is not twelve times slower than a one-step workflow — but it is twelve times the cost. Those are different things and it is easy to conflate them.

Cheap screen, expensive analysis

whole workflow

The highest-value cost pattern: a small model decides whether a large one runs.

{
    "id": "chain_0000000000000041",
    "name": "Gated expense",
    "description": "Most articles stop after one cheap call.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Score how consequential this is for a long-term investor, 0-10. Be strict: most articles are a 2.",
            "stepName": "Screen",
            "id": "step_0000000000000411",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_104_second_order_effects_lens",
            "stepPrompt": "Full second- and third-order analysis.",
            "stepName": "Full analysis",
            "id": "step_0000000000000412",
            "modelId": "claude-sonnet-5",
            "runIf": {
                "step": "step_0000000000000411",
                "field": "score",
                "op": "gte",
                "value": "7"
            }
        }
    ],
    "finalPrompt": "If there was a full analysis, give it. Otherwise, one line on why this was not worth the time.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Set the per-step model explicitly on both steps. Leaving the model blank means the step uses whatever the widget is set to, which defeats the point.
  • 'Full analysis' has no InData. Its condition makes it wait for the screen, and without InData it reads the article — not the screen's score.

Publishing a workflow to the marketplace drops the ceiling from 1000 steps to 100, counted after sub-workflows are inlined, because every step is billed to whoever runs it and a listing must not be able to hand strangers an unbounded bill. Publishing also needs:

  • a nickname and a verified email on your account;
  • every template your steps use to be published — a private template reaches nobody else — and every sub-workflow, and every template inside those;
  • a name no other published workflow uses.

Any Reference text in the workflow is published with it, so do not put anything private in one.

On the free plan you can keep ten workflows of your own (created or forked) and ten installed; the workflows SoWhatify ships with do not count towards either.

12. Troubleshooting

SymptomWhyFix
A step answered about the wrong thing, or said it was given nothing. It is a Prompt step, which never sees the article; or a Template step with an InData source, which replaced the article. Use a Template step with no InData; push other material in with add-to. See wiring.
A gated step analysed the screen’s score instead of the article. It has the screen as InData as well as in its condition. Remove the InData. The condition alone makes it wait.
{$My step} came through literally. Two steps share that name, or it is inside a Reference body. Make names unique, or use the step id. Reference bodies never resolve tokens.
A token vanished, leaving a gap. It named a step that does not exist, a field that was not declared, or a step that had not answered yet. Check the spelling and the shape, and wire the named step upstream — a token alone does not make a step wait.
A step never ran. Its condition was false, everything feeding it was skipped, or its list was empty. The trace names the reason. See conditions.
A gated step ran when it should not have. Its condition reads a step that ran, so an earlier skip did not reach it. Point the condition at the gate that was skipped. See the AND example under conditions.
A repeat ran once. The step is an input step, where the repeat is dropped on save. Move the repeat onto a processing step.
A repeat ran fewer times than expected. The 20-per-step or 100-per-run cap. The run reports how many it managed. Narrow the list upstream.
The output has a placeholder where a sub-workflow should be. The child was deleted, made private, is cyclic, or is too large. See sub-workflows.
A data step produced [DATA UNAVAILABLE…]. The fetch failed, the address was empty, or the runner cannot run that provider. The run carried on without it. Check the address. Fetches are not retried. An address built from a token needs startImmediately: false.
The final answer ignored a step. By default it reads only the leaves, and a step another step reads is not one. Name the step explicitly in the final answer’s inputs.
The final answer repeated a screen’s scores. A step used only by a condition is still a leaf. Choose the final answer’s inputs by hand.
The run failed on a step with a shape. The model missed the shape twice; there is only one retry, and no fallback to prose. Simplify the shape, or use a stronger model for that step.
The workflow will not save. A step that reaches nothing, a reference to a step that does not exist, a cycle, or an empty final-input list. The error names the step. See what fails.
A long prompt or reference came back shorter. Text over its limit is cut to the limit when you save, not refused. See limits.
Publishing was refused. Over 100 steps, an unpublished template or sub-workflow, a name already taken, or no nickname or verified email. See publishing. The workflow stays private rather than half-published.

13. Reference: the workflow object

From here on this is written for authoring workflow JSON directly — through the API, through MCP, or by hand. The machine-readable contract is docs/workflows.schema.json; the normalisers that actually decide what is kept are api/workflows-lib.php and extension/shared/workflows.js, which mirror each other.

There are two workflow shapes. Which one you hold depends on where you got it:

FieldTypeNotes
idstringchain_ or workflow_ + 16 hex characters. Assigned by the server when you create one.
namestringRequired.
descriptionstringMay be empty. Shown in the marketplace.
stepsarrayRequired. 1 to 1000 steps; steps past 1000 are dropped.
finalUserPromptstringThe final answer’s instructions. Required, unless finalPrompt is sent instead.
finalPromptstringThe same text. The server writes finalUserPrompt into both on every save.
finalSystemPromptstringOptional. Falls back to the platform default.
finalModelIdstringOptional. The model for the final answer.
finalInputSourceStepIdsarrayWhich steps the final answer reads. See below.
finalInputIncludesSeedbooleanRe-attach the original source.
categoriesarrayOne marketplace category plus one subcategory or second category. An unknown name is refused.
tagsarrayUp to 12, lower-cased, 40 characters each.
imageUrl, imageUrlFullstringCard and full-size images.
updatedAtstringISO 8601. Set by the server on every save.
ownerUserId, visibility, publishedAt, createdAt—Stored workflows only. visibility is private or public.
runtimeTemplates, runtimePerspectives, runtimeWorkflowsarrayExtension workflows only: the resolved dependencies, carried so a run needs no further lookups.

finalInputSourceStepIds absent means no choice was made, and resolves to the leaf steps — with or without finalInputIncludesSeed. A list means exactly those steps, and every step not on it must feed another step or the save is refused. An empty array is refused unless the seed is re-attached, in which case the final answer reads the article and no steps at all.

The smallest valid workflow

whole workflow

Every field the extensionWorkflow shape requires: id, name, description, steps, finalPrompt, updatedAt. When you create a workflow through the API or MCP, the server assigns the id and updatedAt and description may be empty.

{
    "id": "chain_0000000000000032",
    "name": "Minimal",
    "description": "The least a workflow can be.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Impact",
            "id": "step_0000000000000321"
        }
    ],
    "finalPrompt": "Answer in three lines.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Everything else — categories, tags, images, final model, final inputs — is optional.

Every workflow-level field populated

whole workflow

The complete surface of a workflow object, for authors writing JSON directly.

{
    "id": "chain_0000000000000033",
    "name": "Fully specified",
    "description": "A workflow using every optional field the shape allows.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_077_fact_box_lens",
            "stepPrompt": "Extract the facts.",
            "stepName": "Facts",
            "id": "step_0000000000000331"
        },
        {
            "type": "template",
            "templateId": "market_003_investment_insight_lens",
            "stepPrompt": "Assess the holdings impact.",
            "stepName": "Holdings",
            "id": "step_0000000000000332",
            "inputSourceStepIds": [
                "step_0000000000000331"
            ]
        }
    ],
    "finalPrompt": "Give me the holdings impact, then the facts it rests on.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalUserPrompt": "Give me the holdings impact, then the facts it rests on.",
    "finalSystemPrompt": "You are a portfolio analyst. You never speculate beyond the evidence given.",
    "finalModelId": "claude-sonnet-5",
    "finalInputSourceStepIds": [
        "step_0000000000000332"
    ],
    "finalInputIncludesSeed": true,
    "categories": [
        "Finance & Investing"
    ],
    "tags": [
        "holdings",
        "earnings"
    ],
    "imageUrl": "api/chain-images/example_500.jpg",
    "imageUrlFull": "api/chain-images/example_2000.jpg"
}
  • finalPrompt and finalUserPrompt hold the same text. The server writes finalUserPrompt into both on every save, so send finalUserPrompt; finalPrompt is accepted as a fallback when it is missing.
  • finalInputSourceStepIds absent means 'no choice made' and falls back to the leaf steps. Here it is chosen, so the final answer reads Holdings and the re-attached article, and not Facts.
  • categories keeps one marketplace category plus one subcategory (or a second category), and refuses a name it does not know. tags keeps twelve, lower-cased and cut to 40 characters.

14. Reference: fields every step can have

FieldTypeInput steps?Notes
idstringYesstep_ + 16 hex characters. Omit it and one is assigned; anything malformed is replaced, which breaks whatever referred to it.
typestringYesOne of the five. Missing or empty becomes template; an unknown type is stored and refused at run time.
stepNamestringYesUp to 120 characters. Also what {$Step name} matches.
inputSourceStepIdsarrayNoWhat this step reads. On a Template step, replaces the seed.
addToStepIdsarrayNo (as target)Where this step’s output is pushed.
startImmediatelybooleanYesfalse holds the step until nothing else is ready or running. The strings "false", "0", "no" and "off" count too.
modelIdstringNoBlank means the widget’s model. Not on a sub-workflow step.
outputSchemaobjectNoNamed typed fields. See below.
runIfobjectNoOne condition. See below.
forEachobjectNoRepeat over an upstream list. See below.
flowPositionobjectYes{x, y} in the web flow chart. The extension does not carry it.

inputSourceStepIds, addToStepIds, runIf.step and forEach.step are checked against the workflow’s own steps when you save. Each entry may be a step id or a step’s exact name, if no other step shares it; one that matches no step is refused, naming where it was. So wiring copied between workflows fails loudly rather than disappearing. Only a workflow already in storage has an unknown reference quietly dropped when it is read.

Every common field, in context

whole workflow

The fields any processing step may carry. Shown inside a workflow, because wiring that points at a step which is not present is refused on save.

{
    "id": "chain_0000000000000034",
    "name": "Common fields",
    "description": "One step using every field the common shape allows.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_exposure_mapper",
            "stepPrompt": "List the companies the article affects.",
            "stepName": "Companies",
            "id": "step_0000000000000341",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_003_investment_insight_lens",
            "stepPrompt": "Assess {$item} in detail.",
            "stepName": "Dive",
            "id": "step_0000000000000342",
            "modelId": "claude-sonnet-5",
            "inputSourceStepIds": [
                "step_0000000000000341"
            ],
            "addToStepIds": [
                "step_0000000000000343"
            ],
            "startImmediately": false,
            "outputSchema": {
                "fields": [
                    {
                        "name": "verdict",
                        "type": "enum",
                        "values": [
                            "buy",
                            "hold",
                            "sell"
                        ]
                    }
                ]
            },
            "runIf": {
                "step": "step_0000000000000341",
                "field": "tickers",
                "op": "exists"
            },
            "forEach": {
                "step": "step_0000000000000341",
                "field": "tickers"
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Summarise the verdicts below.",
            "stepName": "Summary",
            "id": "step_0000000000000343"
        }
    ],
    "finalPrompt": "Return the summary.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Input steps (URL and Reference) accept none of modelId, outputSchema, runIf or forEach — they do not call a model, so there is nothing for those fields to do. The normaliser drops them rather than failing the save.
  • addToStepIds, inputSourceStepIds, runIf.step and forEach.step are checked against the workflow's own steps when you save. Each may give a step id or a step's exact, unique name; one that matches no step is refused, which is why this example is a whole workflow rather than a fragment.
  • flowPosition also exists on a stored step. It records where the author dragged the node in the web flow chart; the extension does not carry it.

15. Reference: the five step types

template — requires templateId

templateId is template_ or lens_ + 16 hex characters (a user template), or market_ + a slug of up to 72 lower-case letters, digits and underscores (a marketplace template). stepPrompt is the optional focus, up to 4000 characters. When it is set — or when the step declares an output shape — a second model call rewrites the template’s answer, so it is not free. This is the only step type that reads the seed.

prompt — requires systemPrompt or userPrompt

Both up to 8000 characters, and at least one must be non-empty; a step with both empty is refused. An empty systemPrompt falls back to the platform default. The step never receives the seed: with nothing wired in it gets its prompt alone, and with something wired in it gets the page’s title, address and source type, its prompt, and the wired output.

data — requires provider, dataset

query holds the address or symbol, up to 1000 characters, and template tokens are resolved in it — but a data step cannot be wired, so a token there needs startImmediately: false to find its step answered. form optionally names an SEC form type (up to 16 characters, upper-cased; anything that is not a form code is dropped); empty means the provider’s default.

ProviderDatasetsAvailability
url page_content The web builder and the API.
sec symbol, fundamentals, filing_facts, filing_prose Authored through the API and MCP only, and run by the extension only. An MCP run skips it, a Dropbox refuses a workflow containing one, and a server-side run leaves a marker.
— quote, price_history, news Still accepted by the schema, but no provider offers them since the market-data connector that did was removed. A step using one is saved and then produces a [DATA UNAVAILABLE…] marker at run time. Treat them as unavailable.
mcp — Accepted by the schema, but not wired to a dataset or a runtime handler. A step using it produces a [DATA UNAVAILABLE…] marker without making a network call. Do not rely on it.

The web workflow builder only creates url steps. There is no provider picker in the UI. sec is reachable through the API and MCP, and through hand-written JSON.

It does, however, preserve a sec step it did not create: opening such a workflow in the builder and saving it keeps the provider, dataset and form intact. That was not always true — the builder used to rewrite any data step as a url step on save — so a workflow edited in an older version may need its providers checked.

URL step (provider url)

one step

The only data provider the web builder can produce today.

{
    "type": "data",
    "provider": "url",
    "dataset": "page_content",
    "query": "https://example.com/annual-report",
    "stepName": "Annual report",
    "id": "step_0000000000000351"
}
  • dataset is always page_content for the url provider.

SEC filing facts (API only)

one step

Structured facts from a filing. Reachable through the API and MCP, not the web builder.

{
    "type": "data",
    "provider": "sec",
    "dataset": "filing_facts",
    "query": "AAPL",
    "stepName": "Latest 10-K facts",
    "id": "step_0000000000000352",
    "form": "10-K"
}
  • form is optional and defaults to the provider's own choice (10-K/10-Q). It is upper-cased, and a value that is not a form code is dropped.
  • query carries the symbol or identifier, not a URL.

SEC filing prose (API only)

one step

The narrative sections of a filing rather than its numbers.

{
    "type": "data",
    "provider": "sec",
    "dataset": "filing_prose",
    "query": "MSFT",
    "stepName": "Latest 8-K text",
    "id": "step_0000000000000353",
    "form": "8-K"
}
  • Useful for risk-factor and management-discussion language, which the facts dataset does not carry.

Symbol resolution (API only)

one step

Turning a company name into the identifier the other SEC datasets want.

{
    "type": "data",
    "provider": "sec",
    "dataset": "symbol",
    "query": "Apple Inc",
    "stepName": "Resolve symbol",
    "id": "step_0000000000000354"
}
  • Useful as the first step of a workflow whose later steps need a ticker or CIK rather than a name.
  • A data step that fails leaves a [DATA UNAVAILABLE ...] marker rather than failing the whole run.

Fundamentals (API only)

one step

Company financials, without reading a filing yourself.

{
    "type": "data",
    "provider": "sec",
    "dataset": "fundamentals",
    "query": "MSFT",
    "stepName": "Fundamentals",
    "id": "step_0000000000000355"
}
  • sec is the only provider with a registered connector, and it offers four datasets: symbol, fundamentals, filing_facts and filing_prose.
  • Only the extension runs an sec step. Over MCP it is skipped, a Dropbox refuses a workflow that contains one, and a server-side run leaves a [DATA UNAVAILABLE ...] marker.

text — requires body

body is 1 to 50 000 characters and is emitted verbatim: no model call, no fetch, no token resolution. sourceName (up to 200 characters) records where the text came from and is never injected into a prompt. All bodies in one workflow are capped at 200 000 characters together, because everything the extension stores lives in one browser storage key.

Reference step with provenance

one step

body plus sourceName, which records where the text came from without ever reaching the model.

{
    "type": "text",
    "body": "Section 12(b): No person shall, directly or indirectly, make any untrue statement of a material fact...",
    "sourceName": "securities-act-1933.pdf",
    "stepName": "Statute",
    "id": "step_0000000000000361"
}
  • sourceName is author-facing provenance only. It is never injected into a prompt.
  • In the builder this is filled in for you when you import a PDF: the text is extracted once and stored on the step, so the PDF is never needed again at run time.

workflow — requires workflowId

Nothing else type-specific is stored, and modelId and forEach are not kept. Nesting is capped at three levels, the child’s steps plus one for its final answer count against the parent’s 1000-step budget, and the type never survives to run time — sub-workflows are inlined into a flat list before planning.

16. Reference: shapes, conditions, loops

outputSchema

{ "fields": [ { "name", "type", "values", "description" } ] }. One to 20 fields. name starts with a letter and allows lower-case letters, digits and underscores, up to 40 characters; upper case is lowered. type is one of string, number, boolean, enum or string[]. values is required for enum — up to 20 entries of 80 characters. description is up to 200 characters.

It is deliberately not JSON Schema, so the builder can render it as a form and a later step can compare against it. It is converted to real JSON Schema only when a provider supports native structured output.

runIf

{ "step", "field", "op", "value" }, and nothing else. op is one of eq, ne, contains, gt, gte, lt, lte, exists, isTrue. The last two are unary and take no value. value is at most 200 characters. field is optional; without it the comparison reads the step’s prose.

An unrecognised op makes the whole condition null, which means the step always runs. That is a fail-open, so check your spelling. op itself is matched case-insensitively. contains with an empty value is always true. A runIf whose step is a repeated step is refused.

forEach

{ "step", "field" }. The field must be a string[] the source step actually declared, or the save is refused — but a field name that is not a valid name at all (Bad-Field) drops the whole forEach silently, and the step runs once. A forEach on a sub-workflow step, or over its own step, is refused. One whose source is itself a repeated step is accepted but never runs: there is no single list to repeat over, so it is skipped as for-each-empty.

17. Reference: the execution model

One planning algorithm decides what runs next, in two mirrored implementations: planNextSteps in workflows.js for the extension, and plan_next_steps in workflows-lib.php for the server-side Dropbox runner and the MCP brief. A shared test suite holds them to the same answers. Over MCP, the calling model runs the steps itself from the brief, so conditions, repeat caps and shapes are applied by that model rather than enforced by the server.

Each pass, in order:

  1. Sub-workflows are inlined into a flat step list.
  2. Repeated steps are expanded into one copy per item. This happens on every pass, because it depends on a model’s answer.
  3. Dependencies are built from InData, add-to, and the steps named by runIf and forEach — not from tokens. Cycles are rejected.
  4. Every step whose inputs are ready and whose condition holds is offered at once.
  5. Steps marked startImmediately: false are offered only when nothing else is ready and nothing is in flight.
  6. When no steps remain, the final answer runs.

A step is skipped for one of three reasons, and the trace names which: condition-false, upstream-skipped, or for-each-empty. Parallelism is not a setting — it is whatever the dependency graph allows.

18. Reference: limits

FieldLimitWhat it covers
name 120 Workflow name.
description 2 000 Workflow description, shown in the marketplace.
imageUrl 500 Card image URL.
imageUrlFull 500 Full-size image URL.
stepPrompt 4 000 A template step’s focus text.
stepName 120 A step’s name — also what {$Step name} matches.
systemPrompt 8 000 A prompt step’s system prompt.
userPrompt 8 000 A prompt step’s user prompt.
modelId 120 Per-step model identifier.
finalPrompt 8 000 The final answer’s prompt.
textBody 50 000 One Reference step’s body.
sourceName 200 A Reference step’s provenance label.
textBodyTotal 200 000 All Reference bodies in one workflow, added up.
maxSteps 1 000 Steps in a private workflow.

Caps that live outside that table:

  • 100 steps in a published workflow, against 1000 private, both counted after sub-workflows are inlined.
  • 20 repeats per step, 100 across a run.
  • 3 levels of sub-workflow nesting; each child adds its steps plus one to the budget.
  • 20 fields per output shape; 20 enum values of 80 characters; 200 for a field description.
  • 200 characters for a condition value; 1000 for a data query; 16 for an SEC form.
  • 1000 entries in each of addToStepIds and inputSourceStepIds.
  • 2 categories and 12 tags of 40 characters per workflow.
  • 10 workflows of your own and 10 installed on the free plan.

Text over its limit is cut to the limit when you save, not refused — prompts, focus lines, names, descriptions, Reference bodies and condition values alike. The one length that is refused is the 200 000-character total across Reference bodies. A modelId over 120 characters is dropped rather than cut.

19. Reference: what fails, and what degrades

Refused when you save, with an error naming the step:

  • a missing name, final prompt or step list, or an unknown category;
  • a step that reaches nothing — neither another step nor the final answer;
  • a reference in wiring, a condition or a repeat that matches no step;
  • an empty final-input list without the seed re-attached;
  • a dependency cycle, a step adding to itself, or a sub-workflow that leads back to this workflow;
  • more than 1000 steps once sub-workflows are inlined, or more than 200 000 characters of Reference text;
  • a Prompt step with both prompts empty;
  • a repeat on a sub-workflow step, over itself, or over a field that is not a declared list; a condition on a repeated step.

Quietly adjusted when you save: text over its limit is cut; a malformed step id is replaced; InData on an input step, a step reading itself, and an add-to aimed at an input step are removed; model, shape, condition and repeat are dropped from input steps; a repeat with an invalid field name is dropped.

Degraded at run time, leaving a marker and letting the run finish: an unresolvable sub-workflow, a data step whose fetch failed or whose provider this runner cannot run, and a repeat that exceeded its cap.

Fails the run: a step type this version cannot run, a structured output that could not be parsed after its one retry, and a dependency deadlock.

A context overflow is deliberately not retried — a long Reference body against a small or on-device model will simply fail rather than burn the same tokens twice.

20. Cookbook

Eight complete workflows, each one the server would save as written. They are the shapes most real workflows turn out to be.

Before you publish your own: run it on two different articles — one that works only on the example you designed it against is the usual failure, and a second input shows it at once. Then check what each skipped branch does to the final answer, because a condition that does not hold removes that step entirely.

Cheap screen, expensive analysis

whole workflow

The highest-value cost pattern: a small model decides whether a large one runs.

{
    "id": "chain_0000000000000041",
    "name": "Gated expense",
    "description": "Most articles stop after one cheap call.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Score how consequential this is for a long-term investor, 0-10. Be strict: most articles are a 2.",
            "stepName": "Screen",
            "id": "step_0000000000000411",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_104_second_order_effects_lens",
            "stepPrompt": "Full second- and third-order analysis.",
            "stepName": "Full analysis",
            "id": "step_0000000000000412",
            "modelId": "claude-sonnet-5",
            "runIf": {
                "step": "step_0000000000000411",
                "field": "score",
                "op": "gte",
                "value": "7"
            }
        }
    ],
    "finalPrompt": "If there was a full analysis, give it. Otherwise, one line on why this was not worth the time.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Set the per-step model explicitly on both steps. Leaving the model blank means the step uses whatever the widget is set to, which defeats the point.
  • 'Full analysis' has no InData. Its condition makes it wait for the screen, and without InData it reads the article — not the screen's score.

Router: three exclusive branches

whole workflow

A classifier with an enum shape routing to one of three treatments, all feeding one final answer.

{
    "id": "chain_0000000000000040",
    "name": "Router",
    "description": "One classifier, three mutually exclusive branches.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_131_signal_or_noise",
            "stepPrompt": "Classify the article.",
            "stepName": "Route",
            "id": "step_0000000000000401",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "earnings",
                            "policy",
                            "product"
                        ]
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_003_investment_insight_lens",
            "stepPrompt": "Assess the earnings implications.",
            "stepName": "Earnings branch",
            "id": "step_0000000000000402",
            "runIf": {
                "step": "step_0000000000000401",
                "field": "kind",
                "op": "eq",
                "value": "earnings"
            }
        },
        {
            "type": "template",
            "templateId": "market_007_policy_impact_lens",
            "stepPrompt": "Assess the policy implications.",
            "stepName": "Policy branch",
            "id": "step_0000000000000403",
            "runIf": {
                "step": "step_0000000000000401",
                "field": "kind",
                "op": "eq",
                "value": "policy"
            }
        },
        {
            "type": "template",
            "templateId": "market_006_tech_insight_lens",
            "stepPrompt": "Assess the product implications.",
            "stepName": "Product branch",
            "id": "step_0000000000000404",
            "runIf": {
                "step": "step_0000000000000401",
                "field": "kind",
                "op": "eq",
                "value": "product"
            }
        }
    ],
    "finalPrompt": "Report whichever assessment ran, and name the route that was taken.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Exactly one branch runs. The other two are skipped before any model is called, so a three-way router costs the same as a two-step workflow.
  • No branch has InData: the conditions make them wait for Route, and each reads the article rather than a one-word classification.
  • Route feeds no step through InData or add-to, so it is a leaf as well, and the final answer reads it beside the branch that ran — which is how it can name the route.

News triage

whole workflow

Classify, then fan out one analysis per affected company, then rank.

{
    "id": "chain_000000000000003d",
    "name": "News triage",
    "description": "From one story to a ranked list of exposed names.",
    "steps": [
        {
            "type": "template",
            "templateId": "market_exposure_mapper",
            "stepPrompt": "List every company materially affected by the article.",
            "stepName": "Affected",
            "id": "step_00000000000003d1",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_003_investment_insight_lens",
            "stepPrompt": "Company {$index} of {$count}. Assess the exposure of {$item} specifically, and rate it.",
            "stepName": "Exposure",
            "id": "step_00000000000003d2",
            "addToStepIds": [
                "step_00000000000003d3"
            ],
            "forEach": {
                "step": "step_00000000000003d1",
                "field": "tickers"
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Rank the assessments below from most to least exposed.",
            "stepName": "Rank",
            "id": "step_00000000000003d3"
        }
    ],
    "finalPrompt": "Give me the ranked table, then the one name worth acting on.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Each copy has no InData, so each reads the article with its own focus. forEach alone makes it wait for the list.
  • Cost scales with the list. The screen and each copy are two calls apiece (a shape or a focus adds the second), so a story naming 15 companies is 32 calls plus the ranking and the final answer.

Policy check against house rules

whole workflow

A Reference step pushing the rulebook into a Template step that reads the article, with a conditional escalation.

{
    "id": "chain_000000000000003e",
    "name": "Policy check",
    "description": "The rules live in the workflow, so the check is the same every time.",
    "steps": [
        {
            "type": "text",
            "body": "Editorial standards:\r\n1. Every forecast is attributed to a named person.\r\n2. No unnamed sources for factual claims.\r\n3. Financial advice must carry the standard disclaimer.\r\n4. Corrections are issued within 24 hours.",
            "sourceName": "editorial-standards.md",
            "stepName": "Standards",
            "id": "step_00000000000003e1",
            "addToStepIds": [
                "step_00000000000003e2"
            ]
        },
        {
            "type": "template",
            "templateId": "market_058_compliance_officer_lens",
            "stepPrompt": "Check the article against the standards listed below. For each rule, say pass or fail with the evidence.",
            "stepName": "Compliance check",
            "id": "step_00000000000003e2",
            "outputSchema": {
                "fields": [
                    {
                        "name": "failures",
                        "type": "number",
                        "description": "How many rules failed"
                    },
                    {
                        "name": "worst",
                        "type": "string",
                        "description": "The most serious failure"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Draft the escalation note to the editor.",
            "stepName": "Escalate",
            "id": "step_00000000000003e3",
            "inputSourceStepIds": [
                "step_00000000000003e2"
            ],
            "runIf": {
                "step": "step_00000000000003e2",
                "field": "failures",
                "op": "gte",
                "value": "1"
            }
        }
    ],
    "finalPrompt": "Report the compliance result. Include the escalation note if one was drafted.",
    "finalInputSourceStepIds": [
        "step_00000000000003e2",
        "step_00000000000003e3"
    ],
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputIncludesSeed": true
}
  • The rules are pushed in with add-to rather than pulled with InData, so 'Compliance check' reads the article and the rules together. Pulling them would have replaced the article.
  • The Reference step is free, so the rulebook costs nothing to carry.
  • The compliance result is named in the final inputs because 'Escalate' consumes it, which would otherwise keep it out of the final answer.
  • Publishing this workflow publishes the standards text with it.

Compare two sources

whole workflow

Two URL steps and a comparison, which needs no seed at all.

{
    "id": "chain_000000000000003f",
    "name": "Compare two sources",
    "description": "Same story, two outlets, one diff.",
    "steps": [
        {
            "type": "data",
            "provider": "url",
            "dataset": "page_content",
            "query": "https://example.com/outlet-a",
            "stepName": "Outlet A",
            "id": "step_00000000000003f1"
        },
        {
            "type": "data",
            "provider": "url",
            "dataset": "page_content",
            "query": "https://example.com/outlet-b",
            "stepName": "Outlet B",
            "id": "step_00000000000003f2"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Two accounts of the same event are below. Give me the facts both agree on, the facts only one reports, and any direct contradiction.",
            "stepName": "Diff",
            "id": "step_00000000000003f3",
            "inputSourceStepIds": [
                "step_00000000000003f1",
                "step_00000000000003f2"
            ]
        }
    ],
    "finalPrompt": "Return the comparison, contradictions first.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • One paid call in the whole workflow, plus the final answer. The fetches are free.

Earnings brief

whole workflow

Two input steps feeding a scored assessment, with an expensive deep dive behind a threshold.

{
    "id": "chain_000000000000003c",
    "name": "Earnings brief",
    "description": "Filing facts and fundamentals, screened cheaply, deep-dived only when it matters.",
    "steps": [
        {
            "type": "data",
            "provider": "sec",
            "dataset": "filing_facts",
            "query": "AAPL",
            "stepName": "Filing facts",
            "id": "step_00000000000003c1",
            "form": "10-K"
        },
        {
            "type": "data",
            "provider": "sec",
            "dataset": "fundamentals",
            "query": "AAPL",
            "stepName": "Fundamentals",
            "id": "step_00000000000003c2"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Score how surprising these results are against expectations, 0-10.",
            "stepName": "Surprise score",
            "id": "step_00000000000003c3",
            "modelId": "gpt-4o-mini",
            "inputSourceStepIds": [
                "step_00000000000003c1",
                "step_00000000000003c2"
            ],
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "template",
            "templateId": "market_104_second_order_effects_lens",
            "stepPrompt": "Work through what this result implies for the next two quarters.",
            "stepName": "Deep dive",
            "id": "step_00000000000003c5",
            "modelId": "claude-sonnet-5",
            "inputSourceStepIds": [
                "step_00000000000003c3"
            ],
            "runIf": {
                "step": "step_00000000000003c3",
                "field": "score",
                "op": "gte",
                "value": "6"
            }
        }
    ],
    "finalPrompt": "Lead with the surprise and what it means. If there was no deep dive, say why this quarter was unremarkable.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputSourceStepIds": [
        "step_00000000000003c3",
        "step_00000000000003c5"
    ],
    "finalInputIncludesSeed": true
}
  • The two input steps cost nothing. Only the score and the conditional deep dive are paid calls.
  • The deep dive reads the score's reasoning, not the filings: InData replaces what a Template step reads. Wire the two data steps into it too if it should see the numbers.
  • Both data steps are API-only: the web builder can produce a URL step but not a sec one, and only the extension can run them.
  • Most articles stop after the cheap screen, so the expensive model is bought only when the score earns it.

A workflow of workflows

whole workflow

A parent calling two published sub-workflows, at depth two.

{
    "id": "chain_0000000000000042",
    "name": "Nested",
    "description": "Composition instead of copy-paste.",
    "steps": [
        {
            "type": "workflow",
            "workflowId": "chain_000000000000005c",
            "stepName": "Fundamentals",
            "id": "step_0000000000000421"
        },
        {
            "type": "workflow",
            "workflowId": "chain_000000000000005d",
            "stepName": "Sentiment",
            "id": "step_0000000000000422"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Fundamentals and sentiment are below. Where do they disagree, and which would you trust here?",
            "stepName": "Reconcile",
            "id": "step_0000000000000423",
            "inputSourceStepIds": [
                "step_0000000000000421",
                "step_0000000000000422"
            ]
        }
    ],
    "finalPrompt": "Return the reconciliation.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • Improving either child improves this workflow without touching it.
  • Depth is capped at three, and the children's steps count against this workflow's own step budget.
  • To publish this, both children must be published too, and so must every template they use. A child that is private to whoever runs the parent becomes a marker in the output.

Claim-by-claim verification

whole workflow

Everything at once: a fixed source, a declared list, one check per item, and a synthesis that waits for every copy.

{
    "id": "chain_0000000000000043",
    "name": "Verify every claim",
    "description": "Check the article against sources it does not control, one claim at a time.",
    "steps": [
        {
            "type": "data",
            "provider": "url",
            "dataset": "page_content",
            "query": "https://example.com/primary-source",
            "stepName": "Primary source",
            "id": "step_0000000000000431"
        },
        {
            "type": "template",
            "templateId": "market_extractor",
            "stepPrompt": "List every checkable factual claim the article makes. One per item, quoted.",
            "stepName": "Claims",
            "id": "step_0000000000000432",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "claims",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Claim {$index} of {$count}: \"{$item}\". Check it against the source below. Answer supported, contradicted or unaddressed, and quote the line that settles it.",
            "stepName": "Check",
            "id": "step_0000000000000433",
            "inputSourceStepIds": [
                "step_0000000000000431"
            ],
            "addToStepIds": [
                "step_0000000000000434"
            ],
            "forEach": {
                "step": "step_0000000000000432",
                "field": "claims"
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Every claim has been checked below. Taken together, does the source support what was claimed?",
            "stepName": "Verdict",
            "id": "step_0000000000000434"
        }
    ],
    "finalPrompt": "Give the verdict first, then the contradicted claims, then the unaddressed ones.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputIncludesSeed": true
}
  • The fetch is free; the per-claim checks are not. Twelve claims is twelve checks, plus two calls for the list, the verdict and the final answer.
  • The loop is on the checking step rather than the fetch, because an input step cannot repeat.
  • The checking step pushes into 'Verdict' with add-to, so every copy does, and Verdict waits for all of them however many there turn out to be.
  • Watch the caps: past 20 claims the run reports how many it managed rather than failing.

21. Common questions

A workflow is a set of steps that runs over one source — a web page, a PDF, the clipboard or text you type. Each step produces an answer, steps can feed each other, and a final step combines whatever reaches it into one response. A single template gives you one perspective; a workflow gives you several that build on each other.

Five. Three are processing steps that call a model: a Template step runs a template with an optional focus and is the only step that reads the article, a Prompt step runs your own system and user prompt over whatever is wired into it, and a Sub-workflow step runs another whole workflow. Two are input steps that supply material rather than transforming it: a URL step fetches a page, and a Reference step carries fixed text such as a rulebook or style guide. Input steps never call a model, so they cost nothing to run.

No. A Prompt step gets its own prompt and whatever is wired into it — with something wired in, also the page title and address — but never the article text. To have a model read the article, use a Template step, and wire it into a Prompt step when you want your own prompt to work on what it found.

No. Every step whose inputs are ready runs at the same time, so a workflow is a dependency graph rather than a list. Order comes from wiring and conditions: a step that reads another step, or tests it in a condition, waits for it. Switching on "delay this step to the next batch" holds a step until nothing else is ready or running, but it still sees only what is wired into it — to have a step read what the others wrote, wire them into it.

Because nothing was wired into it. A Template step with no InData source reads the seed — the page, PDF, clipboard or typed text. Choose an InData source and the opposite happens: the step reads that source and no longer sees the article at all. If a step needs both, push the other step's output into it with add-to instead: add-to appends to what the step already reads, so it keeps the article.

No. A step carries at most one condition, built from a step, an optional field, one of nine comparisons, and a value. There is no AND, no OR and no nesting. The simplest way round it is to ask the screening step the combined question as one field and gate on that. Otherwise, express AND by stacking gates: point the second step's condition at the first gate, since a condition on a skipped step is false. Express OR with two sibling branches pushing into one step, which runs if either branch did and is skipped only when both were.

Nine: is, is not, contains, is greater than, is at least, is less than, is at most, has any value, and is yes. The last two are unary and take no value. Text comparisons are trimmed and case-insensitive. The four numeric comparisons are numeric only — if either side is not a number the condition answers false rather than coercing, so "greater than" never quietly means "alphabetically after".

Have an upstream step declare a list field in its output shape, then set the repeated step to repeat over that field. The step is cloned into one copy per item before the workflow is planned, each named after its item, and the tokens {$item}, {$index} and {$count} are substituted into each copy's prompts. There is no loop edge and no collector step: everything that referenced the original now receives every copy. Repeats are capped at 20 per step and 100 per run, and each copy is a separate paid run of the step.

No. Repeating, conditions, output shapes and per-step models all belong to steps that call a model, so an input step accepts none of them — they are dropped when the workflow is saved. There is therefore no way to fetch many addresses from one step. Add a URL step per source, and put the repeat on the step that does the thinking. One address can be computed upstream: put a token in the URL step's address and set it to start later, so it runs once the step that supplies the address has answered.

One per processing step that actually runs, plus one for the final answer. A Template step with a focus or an output shape makes two calls, a repeated step costs one run per copy, and a sub-workflow costs its own calls plus one for its final answer. URL and Reference steps are free because they call no model, and steps whose condition is false are skipped before any call is made. The builder shows a running estimate as you add steps.

No. A Reference step is an input: it takes nothing from upstream and its body is used exactly as written, tokens included. Put the fixed rules in the Reference step and push them with add-to into the step that needs them alongside the article or the upstream output.

From one. In a repeated step, {$item} is the current item, {$index} is its position starting at 1, and {$count} is how many copies run. They are substituted into the system prompt, the user prompt and a template focus. Copies are named after their item automatically, so leave {$item} out of the step name.

The run continues and that step produces a marker saying the sub-workflow was unavailable, rather than failing the whole workflow. The same happens if including the child would form a cycle, if nesting would go past three levels deep, or if inlining it would exceed the step budget. This is the price of a sub-workflow being a live reference rather than a copy: improving the child improves every parent, but a parent can break without changing.

The step is retried exactly once with a correction prompt. If the second attempt is also unparseable the run fails rather than guessing. One retry rather than several is deliberate, because whoever runs the workflow pays for every attempt.

Because every step is a model call billed to whoever runs the workflow, so a marketplace listing must not be able to hand strangers an unbounded bill. Private workflows keep the 1000-step ceiling; both count steps after sub-workflows are inlined. Publishing has other conditions too: your account needs a nickname and a verified email, every template and sub-workflow the workflow uses must itself be published, the name must not be taken, and any Reference text in the workflow is published with it.

They can, but a {$Step name} token then has no single step to resolve to, and neither does a reference by name in wiring or a condition. Keep names unique, or reference the step by its stable id instead.

Still stuck? Support has the contact details, and the workflow builder is where most of this is easier to try than to read.