Chain guide

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

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

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

1. What a chain is

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

Every chain has the same three parts:

  • The seed. The thing being analysed — a web page, an uploaded PDF, the clipboard, or text you type. Every step that has nothing wired into it reads the seed.
  • The steps. One to a thousand of them. 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 chain 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 from nothing else.

2. Your first chain

The builder opens in simple mode: pick up to six lenses, 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 lenses over one article

whole chain

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

{
    "id": "chain_0000000000000001",
    "name": "Two-lens starter",
    "description": "What it means for me, checked against what the article is not saying.",
    "steps": [
        {
            "type": "lens",
            "lensId": "market_001_personal_impact_lens",
            "stepPrompt": "What does this actually change for me? Be concrete.",
            "stepName": "Personal impact",
            "id": "step_0000000000000011"
        },
        {
            "type": "lens",
            "lensId": "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.
  • Three AI calls in total: one per lens, plus the final answer.

Six lenses — the simple-mode ceiling

whole chain

What 'add up to six' costs: six parallel readings plus the synthesis, so seven AI calls.

{
    "id": "chain_0000000000000002",
    "name": "Full sweep",
    "description": "Six angles on one story, combined into a single brief.",
    "steps": [
        {
            "type": "lens",
            "lensId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Personal",
            "id": "step_0000000000000021"
        },
        {
            "type": "lens",
            "lensId": "market_003_investment_insight_lens",
            "stepPrompt": "What changes for my holdings?",
            "stepName": "Investment",
            "id": "step_0000000000000022"
        },
        {
            "type": "lens",
            "lensId": "market_007_policy_impact_lens",
            "stepPrompt": "What changes in policy terms?",
            "stepName": "Policy",
            "id": "step_0000000000000023"
        },
        {
            "type": "lens",
            "lensId": "market_023_critical_analysis_lens",
            "stepPrompt": "Where is the argument weakest?",
            "stepName": "Critical",
            "id": "step_0000000000000024"
        },
        {
            "type": "lens",
            "lensId": "market_104_second_order_effects_lens",
            "stepPrompt": "What follows from this that nobody mentions?",
            "stepName": "Second order",
            "id": "step_0000000000000025"
        },
        {
            "type": "lens",
            "lensId": "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 lens — but it is six times the cost.
  • 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 chain is a normal chain, 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 chain: the steps that feed nothing downstream. That is usually right, because it means the final answer sees conclusions rather than working notes.

What the final answer reads by default

whole chain

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": "lens",
            "lensId": "market_038_editor_in_chief",
            "stepPrompt": "Judge the news value honestly.",
            "stepName": "News value",
            "id": "step_0000000000000041",
            "addToStepIds": [
                "step_0000000000000042"
            ]
        },
        {
            "type": "lens",
            "lensId": "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 chain

finalInputSourceStepIds overrides the leaf default and narrows the final answer to one step.

{
    "id": "chain_0000000000000005",
    "name": "Narrowed final",
    "description": "Three readings, but the final answer is told to use only one of them.",
    "steps": [
        {
            "type": "lens",
            "lensId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Impact",
            "id": "step_0000000000000051"
        },
        {
            "type": "lens",
            "lensId": "market_023_critical_analysis_lens",
            "stepPrompt": "Where is this weak?",
            "stepName": "Critique",
            "id": "step_0000000000000052"
        },
        {
            "type": "lens",
            "lensId": "market_100_action_checklist_lens",
            "stepPrompt": "What should I do?",
            "stepName": "Actions",
            "id": "step_0000000000000053"
        }
    ],
    "finalPrompt": "Give me the action list, nothing else.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputSourceStepIds": [
        "step_0000000000000053"
    ]
}
  • The other two steps still run and still cost money. They simply do not reach the final answer.
  • An absent finalInputSourceStepIds means 'no choice made' and falls back to the leaves. An empty list is a real choice and means none.

Putting the original article back in front of the final answer

whole chain

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": "lens",
            "lensId": "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.

Writing your own final instructions

whole chain

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": "lens",
            "lensId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Impact",
            "id": "step_0000000000000031"
        },
        {
            "type": "lens",
            "lensId": "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
LenslensProcessingYes Runs a lens, optionally narrowed by a focus line.
PromptpromptProcessingYes Runs a raw system and user prompt pair.
Sub-chainchainProcessingVia the child Runs another whole chain and uses its final answer.
URLdataInputNo Fetches a page, or licensed market data through the API.
ReferencetextInputNo Carries fixed text: a rulebook, a statute, a style guide.

Input steps supply data; they do not transform it. A URL or Reference 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 chain 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.

Lens step

one step

A lens with a focus line that narrows a broad lens to this chain's job.

{
    "type": "lens",
    "lensId": "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 lens supplies the perspective; stepPrompt (the builder calls it the focus) narrows it.
  • stepPrompt is a second model call that rewrites the lens output, so it is not free.

Prompt step

one step

A raw system/user prompt pair, when no existing lens fits.

{
    "type": "prompt",
    "systemPrompt": "You are a precise extractor. You never infer, and you never fill gaps.",
    "userPrompt": "List every number in the article with what it measures and the period it covers. If a number has no stated period, say so.",
    "stepName": "Extract numbers",
    "id": "step_0000000000000072"
}
  • 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 chain. 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 chain.
  • Publishing the chain publishes this text. Do not put anything private in a Reference step you intend to share.

Sub-chain step

one step

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

{
    "type": "chain",
    "chainId": "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 deleted, newly private or newly cyclic child 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 chain nothing. 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.

Use whichever reads better in the builder. Pushed output arrives under an ## Added from <step name> heading, so the receiving model can tell the pieces apart.

A step with nothing wired reads the article

whole chain

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

{
    "id": "chain_0000000000000008",
    "name": "Seed default",
    "description": "One step, no wiring, so it reads the article.",
    "steps": [
        {
            "type": "lens",
            "lensId": "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 chain works before you wire anything.

Choosing InData replaces the article. This is the single most common surprise in the builder. A step with nothing wired in reads the seed. The moment you give it an upstream 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.

Choosing InData REPLACES the article

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Summarise the article in five bullet points.",
            "stepName": "Summary",
            "id": "step_0000000000000091"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Rewrite the bullets below 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, add the article back by wiring the seed in, or use a step that keeps it.
  • 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 chain

addToStepIds sends this step's output into the target's input, arriving under an '## Added from ...' heading.

{
    "id": "chain_000000000000000a",
    "name": "Add-to routing",
    "description": "Step one pushes into step two rather than step two pulling from step one.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the claims made in the article.",
            "stepName": "Claims",
            "id": "step_00000000000000a1",
            "addToStepIds": [
                "step_00000000000000a2"
            ]
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Check each claim 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"
}
  • addTo and InData reach the same place from opposite ends. Use whichever reads better in the builder.
  • 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 chain

The case people get wrong: choosing InData drops the article, but anything pushed in with add-to still arrives.

{
    "id": "chain_000000000000000b",
    "name": "Replace and append",
    "description": "Step three picks its own source and is also pushed into.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Summarise the article.",
            "stepName": "Summary",
            "id": "step_00000000000000b1",
            "addToStepIds": [
                "step_00000000000000b3"
            ]
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the risks in the article.",
            "stepName": "Risks",
            "id": "step_00000000000000b2"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Weigh the risks below.",
            "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 chain

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

{
    "id": "chain_000000000000000c",
    "name": "Fan-in",
    "description": "Three independent readings, reconciled by a fourth step.",
    "steps": [
        {
            "type": "lens",
            "lensId": "market_106_red_team_lens",
            "stepPrompt": "Argue against this.",
            "stepName": "Red team",
            "id": "step_00000000000000c1"
        },
        {
            "type": "lens",
            "lensId": "market_107_blue_team_lens",
            "stepPrompt": "Argue for this.",
            "stepName": "Blue team",
            "id": "step_00000000000000c2"
        },
        {
            "type": "lens",
            "lensId": "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 chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Extract the facts, figures and named parties from the article.",
            "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.

Delay a step to the next batch

whole chain

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

{
    "id": "chain_000000000000000e",
    "name": "Deferred reviewer",
    "description": "A reviewer that should see everything else before it runs, without wiring it to every step.",
    "steps": [
        {
            "type": "lens",
            "lensId": "market_001_personal_impact_lens",
            "stepPrompt": "What changes for me?",
            "stepName": "Impact",
            "id": "step_00000000000000e1"
        },
        {
            "type": "lens",
            "lensId": "market_023_critical_analysis_lens",
            "stepPrompt": "Where is this weak?",
            "stepName": "Critique",
            "id": "step_00000000000000e2"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Review everything produced so far and flag any contradiction.",
            "stepName": "Reviewer",
            "id": "step_00000000000000e3",
            "startImmediately": false
        }
    ],
    "finalPrompt": "Report the contradictions, then the impact.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • This is the escape hatch for 'run last' without drawing edges from every step.
  • It is a scheduling hint, not a dependency: the deferred step still receives only what is wired to it.

A prompt step with nothing wired gets no page preamble

whole chain

Wire something in and the step is handed the page with a preamble; wire nothing and it gets your prompt alone.

{
    "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"
}
  • This is how you write a prompt step that ignores the article entirely — wire nothing into it.

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.

Reference a step by its name

whole chain

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

{
    "id": "chain_0000000000000010",
    "name": "Token by name",
    "description": "The final answer names the step it wants.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the risks in the article.",
            "stepName": "Risks",
            "id": "step_0000000000000101"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "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.
  • 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 chain

{$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": "prompt",
            "systemPrompt": "",
            "userPrompt": "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 chain

{$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": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the claims.",
            "stepName": "Claims",
            "id": "step_0000000000000121"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Check the claims.",
            "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.

Reference one field of a declared output shape

whole chain

{$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": "prompt",
            "systemPrompt": "",
            "userPrompt": "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": "This piece 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.
  • The step still produces readable prose too, so {$Classify} on its own keeps working.

Name, id and number in one prompt

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the facts.",
            "stepName": "Facts",
            "id": "step_0000000000000141"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the risks.",
            "stepName": "Risks",
            "id": "step_0000000000000142"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "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 chain next.

A token inside a URL step's address

whole chain

Tokens resolve outside prompts too — a URL step's query is templated, so one step can decide what the next one fetches.

{
    "id": "chain_0000000000000015",
    "name": "Computed fetch",
    "description": "The model picks a URL; the URL step fetches it.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Return only the canonical URL of the company's investor-relations page mentioned in the article. No other text.",
            "stepName": "Find IR page",
            "id": "step_0000000000000151"
        },
        {
            "type": "data",
            "provider": "url",
            "dataset": "page_content",
            "query": "{$Find IR page}",
            "stepName": "Fetch IR page",
            "id": "step_0000000000000152"
        }
    ],
    "finalPrompt": "Summarise the investor-relations page.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • A URL step is an input step, so it takes no InData — but its query is still templated.
  • Anything the model returns goes straight into a fetch, so keep the instruction narrow.

Tokens do not work in a Reference step. Its body is used literally. Resolving a token there would mean a dependency the scheduler cannot see, so it is deliberately not done — 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 static content. Resolving a token in it would mean a dependency the scheduler cannot see, so it is deliberately not done.
  • 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 something that does not exist, a plain one such as {$Missing} resolves to nothing and disappears. One containing a space or a hyphen 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.

The step still writes readable prose as well, so {$Step name} keeps working. You lose nothing by declaring a shape.

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 are lower-case, start with a letter, may contain digits and underscores, and are at most 40 characters. An enum may list up to 20 values of 80 characters each.

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": "prompt",
    "systemPrompt": "",
    "userPrompt": "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.

Reading one field downstream

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Score how consequential this 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": "prompt",
    "systemPrompt": "",
    "userPrompt": "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": "prompt",
    "systemPrompt": "",
    "userPrompt": "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 are lower-case, start with a letter, and allow digits and underscores — up to 40 characters.
  • 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 chain 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 chain 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. It never throws.

Run only if a field equals a value

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Classify this article.",
            "stepName": "Classify",
            "id": "step_00000000000001a1",
            "outputSchema": {
                "fields": [
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion",
                            "analysis"
                        ]
                    }
                ]
            }
        },
        {
            "type": "lens",
            "lensId": "market_023_critical_analysis_lens",
            "stepPrompt": "Fact-check the argument.",
            "stepName": "Fact check",
            "id": "step_00000000000001a2",
            "inputSourceStepIds": [
                "step_00000000000001a1"
            ],
            "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'.
  • A skipped step costs nothing — the model is never called.

Run only if a number clears a threshold

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Score how consequential this story is, 0-10.",
            "stepName": "Screen",
            "id": "step_00000000000001d1",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "lens",
            "lensId": "market_104_second_order_effects_lens",
            "stepPrompt": "Work through the second- and third-order consequences in detail.",
            "stepName": "Deep dive",
            "id": "step_00000000000001d2",
            "inputSourceStepIds": [
                "step_00000000000001d1"
            ],
            "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.

Run only if a boolean is yes

whole chain

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

{
    "id": "chain_000000000000001b",
    "name": "Paywall branch",
    "description": "Only try the workaround when the page is actually paywalled.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Is this article paywalled?",
            "stepName": "Paywall check",
            "id": "step_00000000000001b1",
            "outputSchema": {
                "fields": [
                    {
                        "name": "is_paywalled",
                        "type": "boolean"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Suggest three ways to read this legitimately.",
            "stepName": "Workaround",
            "id": "step_00000000000001b2",
            "inputSourceStepIds": [
                "step_00000000000001b1"
            ],
            "runIf": {
                "step": "step_00000000000001b1",
                "field": "is_paywalled",
                "op": "isTrue"
            }
        }
    ],
    "finalPrompt": "Return the answer.",
    "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": "prompt",
    "systemPrompt": "",
    "userPrompt": "Draft the correction notice.",
    "stepName": "Correction",
    "id": "step_00000000000001c1",
    "runIf": {
        "step": "step_0000000000000161",
        "field": "kind",
        "op": "exists"
    }
}
  • For a list, exists means 'not empty'. For anything else it means 'not blank'.

Everything except one case

one step

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

{
    "type": "lens",
    "lensId": "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": "prompt",
    "systemPrompt": "",
    "userPrompt": "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. A condition that needs two tests has to be split across two steps — which sounds like a limitation and mostly reads better anyway, because each gate ends up named.

Two conditions (AND), expressed as a chain

whole chain

There is no AND. Chain the gates: B only runs if A ran, so B's condition is effectively A AND B.

{
    "id": "chain_0000000000000020",
    "name": "Chained gates",
    "description": "High score AND opinion — as two steps, each carrying one condition.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Classify and score this article.",
            "stepName": "Screen",
            "id": "step_0000000000000201",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    },
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion"
                        ]
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Confirm this is worth a full review.",
            "stepName": "Gate 1",
            "id": "step_0000000000000202",
            "inputSourceStepIds": [
                "step_0000000000000201"
            ],
            "runIf": {
                "step": "step_0000000000000201",
                "field": "score",
                "op": "gte",
                "value": "7"
            }
        },
        {
            "type": "lens",
            "lensId": "market_023_critical_analysis_lens",
            "stepPrompt": "Full critical review.",
            "stepName": "Review",
            "id": "step_0000000000000203",
            "inputSourceStepIds": [
                "step_0000000000000202"
            ],
            "runIf": {
                "step": "step_0000000000000201",
                "field": "kind",
                "op": "eq",
                "value": "opinion"
            }
        }
    ],
    "finalPrompt": "Report the review if there was one.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • 'Review' depends on 'Gate 1'. If Gate 1 is skipped, Review is skipped too — that is the AND.
  • A step whose every input was skipped is skipped as well, which is what makes this work.

Two conditions (OR), expressed as siblings

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Classify and score this article.",
            "stepName": "Screen",
            "id": "step_0000000000000211",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    },
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "news",
                            "opinion"
                        ]
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Explain why the score is high.",
            "stepName": "High score branch",
            "id": "step_0000000000000212",
            "inputSourceStepIds": [
                "step_0000000000000211"
            ],
            "addToStepIds": [
                "step_0000000000000214"
            ],
            "runIf": {
                "step": "step_0000000000000211",
                "field": "score",
                "op": "gte",
                "value": "8"
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Explain the argument being made.",
            "stepName": "Opinion branch",
            "id": "step_0000000000000213",
            "inputSourceStepIds": [
                "step_0000000000000211"
            ],
            "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.",
    "updatedAt": "2026-09-12T10:00:00Z"
}
  • 'Desk note' has no condition, so it runs if anything reached it.
  • Careful: it also runs when neither branch fired. If that matters, give it its own condition too.

Both patterns rest on the same rule: skips travel downstream. A step whose inputs were all skipped is skipped too, and a skipped branch never reaches the final answer. The run trace names the reason for each one — condition-false for the step that failed its own test, and upstream-skipped for everything that depended on it.

Skips travel downstream

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Score this.",
            "stepName": "Screen",
            "id": "step_0000000000000221",
            "outputSchema": {
                "fields": [
                    {
                        "name": "score",
                        "type": "number"
                    }
                ]
            }
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Deep analysis.",
            "stepName": "Analysis",
            "id": "step_0000000000000222",
            "inputSourceStepIds": [
                "step_0000000000000221"
            ],
            "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"
}
  • '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.

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 items there are in total.

They are substituted into the system prompt, the user prompt, a lens focus, a URL step’s address, and the step name — and nowhere else. Putting {$item} in the step name is worth doing: it is what makes the flow chart and the combined output readable.

There is no loop edge and no collector step. The step is cloned into N steps before the chain 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 chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Identify every company materially affected.",
            "stepName": "Find companies",
            "id": "step_0000000000000231",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "lens",
            "lensId": "market_003_investment_insight_lens",
            "stepPrompt": "Company {$index} of {$count}: assess the impact on {$item} specifically.",
            "stepName": "Dive: {$item}",
            "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 the total.
  • Those three tokens are substituted into the system prompt, user prompt, focus, URL query and step name — nowhere else.
  • Each repeat is a separate paid model call. Twelve tickers means twelve calls.
  • Capped at 20 repeats per step and 100 across a run. Past that the run reports 'ran 20 of 63' rather than failing.

Naming the copies

one step

Putting {$item} in the step name makes the flow chart and the output headings readable.

{
    "type": "lens",
    "lensId": "market_003_investment_insight_lens",
    "stepPrompt": "Assess {$item}.",
    "stepName": "Dive: {$item}",
    "id": "step_0000000000000241",
    "forEach": {
        "step": "step_0000000000000231",
        "field": "tickers"
    }
}
  • Without this every copy is called the same thing, and the combined output is hard to read.

Every copy reaches the step that consumed the original

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the affected companies.",
            "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.
  • 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 chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "List affected companies, 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 chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "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: {$item}",
            "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. Either add a URL step per source, or have one URL step's query computed by an upstream model.
  • 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.

10. Sub-chains

A sub-chain step runs another whole chain 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 four ways that happens: the child was deleted, it became private, including it would form a cycle, or inlining it would go past three levels deep or blow the step budget.

Calling another chain

whole chain

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

{
    "id": "chain_0000000000000028",
    "name": "Parent with a sub-chain",
    "description": "Reusing a standard analysis instead of copying its steps.",
    "steps": [
        {
            "type": "chain",
            "chainId": "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",
    "runtimeChains": [
        {
            "id": "chain_000000000000005a",
            "name": "Standard analysis",
            "steps": [
                {
                    "type": "lens",
                    "lensId": "market_001_personal_impact_lens",
                    "stepPrompt": "What changes?",
                    "stepName": "Impact",
                    "id": "step_00000000000005a1"
                },
                {
                    "type": "lens",
                    "lensId": "market_023_critical_analysis_lens",
                    "stepPrompt": "Where is it weak?",
                    "stepName": "Critique",
                    "id": "step_00000000000005a2"
                }
            ],
            "finalUserPrompt": "Combine the two readings."
        }
    ]
}
  • runtimeChains carries the resolved child when the chain 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 count against the parent's step budget.

When a sub-chain cannot be resolved

whole chain

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-chain",
    "description": "The parent still produces an answer, and says what was missing.",
    "steps": [
        {
            "type": "chain",
            "chainId": "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-CHAIN UNAVAILABLE: ...] and the run carries on.
  • Four causes: the child was deleted, it became private, it would form a cycle, 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 chain costs

Each step can pick its own model, or leave it blank to use whatever the widget is set to. The arithmetic is simple: one call per processing step that actually runs, plus one for the final answer. URL and Reference steps are free. Skipped steps are free. A repeated step costs one call per item.

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

Cheap screen, expensive analysis

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "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": "lens",
            "lensId": "market_104_second_order_effects_lens",
            "stepPrompt": "Full second- and third-order analysis.",
            "stepName": "Full analysis",
            "id": "step_0000000000000412",
            "modelId": "claude-sonnet-5",
            "inputSourceStepIds": [
                "step_0000000000000411"
            ],
            "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.

Publishing a chain to the marketplace drops the ceiling from 1000 steps to 100, because every step is billed to whoever runs it and a listing must not be able to hand strangers an unbounded bill. Publishing also requires that every lens your steps use is published too — a private step lens reaches nobody else, and the run would fail on it. And any Reference text in the chain is published with it, so do not put anything private in one.

12. Troubleshooting

SymptomWhyFix
A step answered about the wrong thing. It has an InData source, so it never saw the article. Wire the article back in, or remove the source. See wiring.
{$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, or a field that was not declared. Check the spelling, and that the source step declares that field.
A step never ran. Its condition was false, or everything feeding it was skipped. The trace names the reason. See conditions.
A repeat ran once. The source field is not string[], or the step is an input step. Declare the field as a list; 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-chain should be. The child was deleted, made private, is cyclic, or is too large. See sub-chains.
A data step produced [DATA UNAVAILABLE…]. The fetch failed and the run carried on without it. Check the address. Retryable failures are already retried.
The final answer ignored a step. By default it reads only the leaves. Name the step explicitly in the final answer’s inputs.
Structured fields came back as prose. The model missed the shape twice; there is only one retry. Simplify the shape, or use a stronger model for that step.
The chain will not save. A cycle in the wiring, or a field over its limit. See limits. Cycles are rejected at save time.
Publishing was refused. Over 100 steps, an unpublished step lens, or no nickname or verified email. See publishing. The chain stays private rather than half-published.

13. Reference: the chain object

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

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

FieldTypeNotes
idstringchain_ + 16 hex characters.
namestringRequired.
descriptionstringRequired. Shown in the marketplace.
stepsarrayRequired. 1 to 1000 steps.
finalPromptstringRequired. The stored default synthesis prompt.
finalUserPromptstringWhat the author typed. Takes precedence.
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.
categoriesarrayUp to 4, from the marketplace category list.
tagsarrayUp to 50, 80 characters each.
imageUrl, imageUrlFullstringCard and full-size images.
updatedAtstringRequired. ISO 8601.
ownerUserId, visibility, publishedAt, createdAtStored chains only. visibility is private or public.
runtimeLenses, runtimePersonas, runtimeChainsarrayExtension chains 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. An empty array is a real choice and means none. They are not the same thing, and only one of them is what you meant.

The smallest valid chain

whole chain

Every field the extensionChain shape actually requires: id, name, description, steps, finalPrompt, updatedAt.

{
    "id": "chain_0000000000000032",
    "name": "Minimal",
    "description": "The least a chain can be.",
    "steps": [
        {
            "type": "lens",
            "lensId": "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 chain-level field populated

whole chain

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

{
    "id": "chain_0000000000000033",
    "name": "Fully specified",
    "description": "A chain using every optional field the shape allows.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Extract the facts.",
            "stepName": "Facts",
            "id": "step_0000000000000331"
        },
        {
            "type": "lens",
            "lensId": "market_003_investment_insight_lens",
            "stepPrompt": "Assess the holdings impact.",
            "stepName": "Holdings",
            "id": "step_0000000000000332",
            "inputSourceStepIds": [
                "step_0000000000000331"
            ]
        }
    ],
    "finalPrompt": "Default synthesis instructions, used when no user prompt is set.",
    "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 is the stored default; finalUserPrompt is what the author typed. Both exist, and the user prompt wins.
  • finalInputSourceStepIds absent means 'no choice made' and falls back to the leaf steps. An empty array means 'none', which is a different thing.
  • categories takes up to 4 values from the marketplace list; tags up to 50.

14. Reference: fields every step can have

FieldTypeInput steps?Notes
idstringYesstep_ + 16 hex characters. Anything else is rejected.
typestringYesOne of the five. Missing or empty becomes lens.
stepNamestringYesUp to 120 characters. Also what {$Step name} matches.
inputSourceStepIdsarrayNoWhat this step reads. Replaces the seed.
addToStepIdsarrayNo (as target)Where this step’s output is pushed.
startImmediatelybooleanYesfalse holds the step to a later batch.
modelIdstringNoBlank means the widget’s model.
outputSchemaobjectNoNamed typed fields. See below.
runIfobjectNoOne condition. See below.
forEachobjectNoRepeat over an upstream list. See below.
flowPositionobjectYes{x, y} in the flow chart. Written by the server only; the extension does not carry it back.

Both id lists are checked against the chain’s own steps. An id that is not in the chain is dropped rather than failing the save, so wiring copied between chains silently disappears — check it after a copy.

Every common field, in context

whole chain

The fields any processing step may carry. Shown inside a chain, because wiring that points at steps which are not present is dropped on save.

{
    "id": "chain_0000000000000034",
    "name": "Common fields",
    "description": "One step using every field the common shape allows.",
    "steps": [
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "List the affected companies.",
            "stepName": "Companies",
            "id": "step_0000000000000341",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "lens",
            "lensId": "market_003_investment_insight_lens",
            "stepPrompt": "Assess {$item} in detail.",
            "stepName": "Dive: {$item}",
            "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 and inputSourceStepIds are checked against the chain's own step ids. An id that is not in the chain is dropped, which is why this example is a whole chain rather than a fragment.
  • flowPosition also exists on a stored step. It records where the author dragged the node in the flow chart, is written by the server only, and the extension does not carry it back.

15. Reference: the five step types

lens — requires lensId, stepPrompt

lensId is either lens_ + 16 hex characters (a user lens) or market_ + a slug (a marketplace lens). stepPrompt is the focus, up to 4000 characters — and it is a second model call that rewrites the lens output, so it is not free.

prompt — requires systemPrompt, userPrompt

Both up to 8000 characters. An empty systemPrompt falls back to the platform default. A prompt step with both fields empty is dropped entirely.

data — requires provider, dataset

query holds the address or symbol, up to 1000 characters, and template tokens are resolved in it. form optionally names an SEC form type (up to 16 characters); empty means the provider’s default.

ProviderDatasetsAvailability
url page_content The web builder and the API.
sec symbol, fundamentals, filing_facts, filing_prose API and MCP only.
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 fails 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 chain 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 chain 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 chain 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).
  • 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 chain 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.

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 chain 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.

chain — requires chainId

Nothing else is stored. Nesting is capped at three levels, the child’s steps count against the parent’s 1000-step budget, and the type never survives to run time — sub-chains are inlined into a flat list before planning.

16. Reference: shapes, conditions, loops

outputSchema

{ "fields": [ { "name", "type", "values", "description" } ] }. One to 20 fields. name is lower-case, starts with a letter, allows digits and underscores, up to 40 characters. 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.

forEach

{ "step", "field" }. The field must be a string[] the source step actually declared. A forEach on a sub-chain step is refused, and one whose source is itself a repeated step is refused too — there is no single answer to iterate over.

17. Reference: the execution model

One planner decides what runs next, and every runner uses it — the extension, the server-side Dropbox runner, and the MCP brief. They cannot disagree, because the decision lives in one place.

Each pass, in order:

  1. Sub-chains 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. 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 Chain name.
description 2 000 Chain description, shown in the marketplace.
imageUrl 500 Card image URL.
imageUrlFull 500 Full-size image URL.
stepPrompt 4 000 A lens 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 chain, added up.
maxSteps 1 000 Steps in a private chain.

Caps that live outside that table:

  • 100 steps in a published chain, against 1000 private.
  • 20 repeats per step, 100 across a run.
  • 3 levels of sub-chain nesting; inlined steps share the 1000-step budget.
  • 20 fields per output shape; 20 enum values of 80 characters.
  • 200 characters for a condition value; 1000 for a data query; 16 for an SEC form.
  • 1000 entries in each of addToStepIds and inputSourceStepIds.
  • 4 categories and 50 tags per chain.

19. Reference: what fails, and what degrades

Rejected when you save: a dependency cycle, a duplicate or malformed step id, a step that feeds itself, an input step given an input, a field over its limit, a repeat on a sub-chain step or over a field that is not a declared list.

Degraded at run time, leaving a marker and letting the run finish: an unresolvable sub-chain, a data step whose fetch failed, 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 chains, each valid as written. They are the shapes most real chains turn out to be.

Cheap screen, expensive analysis

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "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": "lens",
            "lensId": "market_104_second_order_effects_lens",
            "stepPrompt": "Full second- and third-order analysis.",
            "stepName": "Full analysis",
            "id": "step_0000000000000412",
            "modelId": "claude-sonnet-5",
            "inputSourceStepIds": [
                "step_0000000000000411"
            ],
            "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.

Router: three exclusive branches

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "Classify this article.",
            "stepName": "Route",
            "id": "step_0000000000000401",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "kind",
                        "type": "enum",
                        "values": [
                            "earnings",
                            "policy",
                            "product"
                        ]
                    }
                ]
            }
        },
        {
            "type": "lens",
            "lensId": "market_003_investment_insight_lens",
            "stepPrompt": "Assess the earnings implications.",
            "stepName": "Earnings branch",
            "id": "step_0000000000000402",
            "inputSourceStepIds": [
                "step_0000000000000401"
            ],
            "runIf": {
                "step": "step_0000000000000401",
                "field": "kind",
                "op": "eq",
                "value": "earnings"
            }
        },
        {
            "type": "lens",
            "lensId": "market_007_policy_impact_lens",
            "stepPrompt": "Assess the policy implications.",
            "stepName": "Policy branch",
            "id": "step_0000000000000403",
            "inputSourceStepIds": [
                "step_0000000000000401"
            ],
            "runIf": {
                "step": "step_0000000000000401",
                "field": "kind",
                "op": "eq",
                "value": "policy"
            }
        },
        {
            "type": "lens",
            "lensId": "market_006_tech_insight_lens",
            "stepPrompt": "Assess the product implications.",
            "stepName": "Product branch",
            "id": "step_0000000000000404",
            "inputSourceStepIds": [
                "step_0000000000000401"
            ],
            "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 chain.
  • All three branches are leaves, so the final answer reads whichever produced output.

News triage

whole chain

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": "prompt",
            "systemPrompt": "",
            "userPrompt": "List every company materially affected by this story.",
            "stepName": "Affected",
            "id": "step_00000000000003d1",
            "modelId": "gpt-4o-mini",
            "outputSchema": {
                "fields": [
                    {
                        "name": "tickers",
                        "type": "string[]"
                    }
                ]
            }
        },
        {
            "type": "lens",
            "lensId": "market_003_investment_insight_lens",
            "stepPrompt": "Company {$index} of {$count}. Assess the exposure of {$item} specifically, and rate it.",
            "stepName": "Exposure: {$item}",
            "id": "step_00000000000003d2",
            "inputSourceStepIds": [
                "step_00000000000003d1"
            ],
            "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"
}
  • Cost scales with the list. A story naming 15 companies is 17 model calls.

Policy check against house rules

whole chain

A Reference step carrying the rulebook, compared against the article, with a conditional escalation.

{
    "id": "chain_000000000000003e",
    "name": "Policy check",
    "description": "The rules live in the chain, 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"
        },
        {
            "type": "prompt",
            "systemPrompt": "",
            "userPrompt": "Check the article against the standards below. For each rule, say pass or fail with the evidence.",
            "stepName": "Compliance check",
            "id": "step_00000000000003e2",
            "inputSourceStepIds": [
                "step_00000000000003e1"
            ],
            "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.",
    "updatedAt": "2026-09-12T10:00:00Z",
    "finalInputIncludesSeed": true
}
  • The Reference step is free, so the rulebook costs nothing to carry.
  • Publishing this chain publishes the standards text with it.

Compare two sources

whole chain

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 chain, plus the final answer. The fetches are free.

Earnings brief

whole chain

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": "lens",
            "lensId": "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",
    "finalInputIncludesSeed": true
}
  • The two input steps cost nothing. Only the score and the conditional deep dive are paid calls.
  • Both data steps are API-only: the web builder can produce a URL step but not a sec one.
  • Most articles stop after the cheap screen, so the expensive model is bought only when the score earns it.

A chain of chains

whole chain

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

{
    "id": "chain_0000000000000042",
    "name": "Nested",
    "description": "Composition instead of copy-paste.",
    "steps": [
        {
            "type": "chain",
            "chainId": "chain_000000000000005c",
            "stepName": "Fundamentals",
            "id": "step_0000000000000421"
        },
        {
            "type": "chain",
            "chainId": "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 chain without touching it.
  • Depth is capped at three, and the children's steps count against this chain's own step budget.
  • To publish this, both children must be published too — a private child reaches nobody else and the run fails on it.

Claim-by-claim verification

whole chain

Everything at once: fixed sources, a declared list, one analysis per item, and a deferred synthesis.

{
    "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": "prompt",
            "systemPrompt": "",
            "userPrompt": "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: {$item}",
            "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. Is the article supported by the source it cites?",
            "stepName": "Verdict",
            "id": "step_0000000000000434",
            "startImmediately": false
        }
    ],
    "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 fourteen paid calls.
  • The loop is on the checking step rather than the fetch, because an input step cannot repeat.
  • 'Verdict' is deferred rather than wired to every copy, which keeps it correct however many claims 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 chain is an ordered pipeline 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 lens gives you one perspective; a chain gives you several that build on each other.

Five. Three are processing steps that transform what came before: a Lens step runs a lens with an optional focus, a Prompt step runs a raw system and user prompt pair, and a Sub-chain step runs another whole chain. Two are input steps that supply data 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. Every step whose inputs are ready runs at the same time, so a chain is a dependency graph rather than a list. Order emerges from wiring: a step that reads another step waits for it. If you need a step to run last without wiring it to everything, switch on "delay this step to the next batch" and it is held until nothing else is ready and nothing is still running.

Because nothing was wired into it. A step with no upstream source reads the seed — the page, PDF, clipboard or typed text. Choose an InData source and the opposite happens: the step now reads that source and no longer sees the article at all. That replacement is the single most common surprise in the chain builder. If a step needs both, wire the article back in or switch on the final answer option that re-attaches the seed.

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, because the condition is a closed set of comparisons rather than an expression language — there is nothing to parse and nothing to sandbox. Express AND by chaining two gated steps, since a step whose inputs were all skipped is skipped too. Express OR with two sibling branches feeding one step, since a skipped branch simply contributes nothing.

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 chain is planned, and the tokens {$item}, {$index} and {$count} are substituted into each copy. 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 repeat is a separate paid model call.

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 chain is saved. There is therefore no way to fetch many addresses from one step. Add a URL step per source, or have one URL step whose address is computed by an upstream model, and put the loop on the step that does the thinking.

One per processing step that actually runs, plus one for the final answer. URL and Reference steps are free because they call no model. Steps whose condition is false are skipped before any call is made, so a three-way router costs the same as a two-step chain. A repeated step costs one call per item. The builder shows a running estimate as you add steps.

No. A Reference step body is used literally and tokens in it are never resolved, because a static reference that interpolated upstream output would need a dependency the scheduler cannot see. Put the fixed rules in the Reference step and do the mixing in a prompt step that reads both it and the upstream output.

From one. In a repeated step, {$item} is the current item, {$index} is its position starting at 1, and {$count} is the total number of items. They are substituted into the system prompt, the user prompt, a lens focus, a URL step query and the step name — nowhere else.

The run continues and that step produces a marker saying the sub-chain was unavailable, rather than failing the whole chain. 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-chain 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 chain pays for every attempt.

Because every step is a model call billed to whoever runs the chain, so a marketplace listing must not be able to hand strangers an unbounded bill. Private chains keep the 1000-step ceiling. Publishing has other conditions too: every lens a step uses must itself be published, and any Reference text in the chain is published with it.

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

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