Authoring WARPP Workflows
WARPP is Manifold's typed-port dataflow engine. A workflow is a directed
acyclic graph of nodes. Data moves only through typed wires or literals —
there are no expressions. Your job is to produce a valid workflow document
(JSON) and prove it valid with workflow_save.
The one rule
Every node is a pure function with declared, typed input and output ports. An
input binding is exactly one of:
{"from": "nodeID.port"} — a wire from an upstream node's output port
{"value": <literal>} — a fixed literal typed into the node
Never both. Never an expression string, template, or ${...}. To reshape data,
use a data node (data.extract, data.template, data.parse, …).
The authoring loop (do this every time)
- Discover tools with
tool_search. For every step that performs an
external action (web search/fetch, file I/O, calling an API or another
agent, sending a message, RAG, an MCP capability, …), call tool_search
with a natural-language description of the capability you need — e.g.
"search the web", "read a file", "query a database",
"send a slack message". The user may have configured MCP servers that
publish tools you cannot know ahead of time, so do not assume a tool
exists or guess its name — search, then pick the best-matching result for
the step. If several match, choose the most specific/appropriate one.
workflow_catalog — fetch the live node manifests for the tools you
chose (plus the builtins), their exact port names/types, and the coercion
table. Every registry tool is available as a node typed tool.<name>.
Never guess a node type or a port name — read them from the catalog.
- Build the document (shape below). Prefer wiring (
from) over literals
for anything that should be dynamic.
workflow_save — it validates and returns diagnostics. If any come
back, fix each one (they carry a code, message, and path) and save
again. Do not stop until it saves with no error diagnostics.
workflow_run (only if asked to test) — runs it and returns the
declared outputs.
Discover → build → validate → self-correct. Do not hand back an unvalidated
document, and do not invent a tool you did not find via tool_search or the
catalog.
Document shape
{
"id": "research-brief",
"name": "Research brief",
"inputs": [{ "name": "topic", "type": "text", "required": true }],
"nodes": [
{ "id": "search", "type": "tool.web_search",
"inputs": { "query": { "from": "in.topic" }, "max_results": { "value": 5 } } },
{ "id": "summary", "type": "llm.generate",
"inputs": { "instruction": { "value": "Summarize as 5 bullets." },
"input": { "from": "search.results_text" } } }
],
"outputs": { "brief": { "from": "summary.text" } }
}
id matches ^[a-zA-Z0-9_-]+$; name is human-readable.
inputs are the workflow's typed parameters, exposed to nodes as
in.<name> (e.g. {"from": "in.topic"}).
- Each node has a unique
id, a type from the catalog, and an inputs map
keyed by the manifest's input port names.
outputs maps result names to node output ports; this is the run result.
in and item are reserved node ids.
- Optional:
settings.max_concurrency (default 4); publish.tool: true to
expose the workflow as an agent tool; project_id to scope file tools to a
project.
Types and coercions
Port types: text, number, boolean, json, file, list<T> (T is any
scalar). The only implicit coercions, applied to wires, are:
number → text
boolean → text
Nothing else coerces. json → text needs data.stringify; text → json needs
data.parse; pull a field out of json with data.extract. Literals must
match the port type exactly ("5" for a text port, 5 for a number port). A
file port accepts a literal path string.
Builtin nodes (stable; always available)
| Type |
Inputs |
Outputs |
data.extract |
source: json, path: text, as: text (default "json") |
value (typed by as) |
data.template |
template: text, vars (named, any type) |
text: text |
data.merge |
objects (list of json) |
json: json |
data.stringify |
value: T |
text: text |
data.parse |
text: text |
json: json |
data.constant |
value: json, as: text (default "json") |
value (typed by as) |
logic.if |
condition: boolean, value: T |
then: T, else: T (one fires) |
logic.coalesce |
values (list of T) |
value: T (first that fired) |
logic.equals |
a: T, b: T |
result: boolean |
logic.contains |
haystack: text, needle: text |
result: boolean |
logic.not |
value: boolean |
result: boolean |
logic.greater_than |
a: number, b: number |
result: boolean |
control.map |
items: list<T>, concurrency: number, on_item_error: text |
results: list<…> |
llm.generate |
instruction: text, input: text, model: text |
text: text |
Tool nodes are tool.<name> — one for every tool in the registry,
including MCP-server tools (e.g. tool.web_search, tool.file_write,
tool.brave_brave_web_search). The set is instance-specific and not known in
advance, so find tools with tool_search (by capability) rather than
assuming; then use the match as node type tool.<its name>. Each tool node has
a result: json output plus, for curated tools, typed outputs. Get the exact
input/output ports from workflow_catalog.
data.extract / data.constant set their output type via the as config:
one of text, number, boolean, json, list<json>.
Named-variadic and list-variadic ports
data.template.vars is named-variadic: an object of bindings, one per
{placeholder} in the template —
"vars": { "topic": {"from":"in.topic"}, "n": {"value": 3} }.
logic.coalesce.values and data.merge.objects are list-variadic: an
array of bindings — "values": [ {"from":"a.text"}, {"from":"b.text"} ].
Control flow (gated wires, not conditionals)
logic.if fires exactly one of then / else per run based on
condition.
- A node whose required input never fires (e.g. it reads the branch that
didn't fire) is skipped, and skips cascade downstream. An optional
input whose source is skipped falls back to its default.
logic.coalesce rejoins branches: it emits the first of its inputs that
actually fired.
control.map fans out over items: list<T>: its body subgraph runs once
per item, seeing item.value and item.index, and gathers the body's single
result output into results. on_item_error is fail (default) or
skip. Bodies may also wire from outer / in ports.
Common mistakes to avoid
- Assuming a tool exists or guessing its name. Use
tool_search to find the
right tool (MCP servers add tools you can't know in advance), then read its
exact ports from workflow_catalog.
- Using an expression / template string in a binding. Use a
data.* node.
- Wiring
json into a text port (or vice-versa) without a
data.stringify / data.parse / data.extract between them.
- A literal whose JSON type doesn't match the port (e.g.
5 into a text port).
- Forgetting
data.template.vars is a named object and
logic.coalesce.values is an array.
- Returning a document without running it through
workflow_save.
More examples
Read references/examples.md (via skill_read, path references/examples.md)
for full worked documents: a linear pipeline, an if/coalesce branch, and a map
fan-out.
1---
2name: warpp-workflow-authoring
3description: Author WARPP workflows for Manifold as typed-port dataflow JSON. Use when a user asks to build, create, design, generate, or edit a Flow / workflow / automation / pipeline / DAG in Manifold, or mentions WARPP, workflow nodes, or the /flow builder. Covers the document format, port types and coercions, the builtin node catalog, control flow (if/coalesce/map), and the workflow_catalog then workflow_save validate-and-self-correct loop.
4metadata:
5 short-description: Build and validate WARPP workflow JSON documents for Manifold.
6---
7
8# Authoring WARPP Workflows
9
10WARPP is Manifold's typed-port dataflow engine. A workflow is a directed
11acyclic graph of nodes. **Data moves only through typed wires or literals —
12there are no expressions.** Your job is to produce a valid workflow *document*
13(JSON) and prove it valid with `workflow_save`.
14
15## The one rule
16
17Every node is a pure function with declared, typed input and output ports. An
18input binding is **exactly one of**:
19
20- `{"from": "nodeID.port"}` — a wire from an upstream node's output port
21- `{"value": <literal>}` — a fixed literal typed into the node
22
23Never both. Never an expression string, template, or `${...}`. To reshape data,
24use a data node (`data.extract`, `data.template`, `data.parse`, …).
25
26## The authoring loop (do this every time)
27
281. **Discover tools with `tool_search`.** For every step that performs an
29 external action (web search/fetch, file I/O, calling an API or another
30 agent, sending a message, RAG, an MCP capability, …), call `tool_search`
31 with a natural-language description of the capability you need — e.g.
32 `"search the web"`, `"read a file"`, `"query a database"`,
33 `"send a slack message"`. The user may have configured MCP servers that
34 publish tools you cannot know ahead of time, so **do not assume a tool
35 exists or guess its name** — search, then pick the best-matching result for
36 the step. If several match, choose the most specific/appropriate one.
372. **`workflow_catalog`** — fetch the live node manifests for the tools you
38 chose (plus the builtins), their exact port names/types, and the coercion
39 table. Every registry tool is available as a node typed `tool.<name>`.
40 **Never guess a node type or a port name — read them from the catalog.**
413. **Build the document** (shape below). Prefer wiring (`from`) over literals
42 for anything that should be dynamic.
434. **`workflow_save`** — it validates and returns diagnostics. If any come
44 back, fix each one (they carry a `code`, `message`, and `path`) and save
45 again. Do not stop until it saves with no error diagnostics.
465. **`workflow_run`** (only if asked to test) — runs it and returns the
47 declared outputs.
48
49Discover → build → validate → self-correct. Do not hand back an unvalidated
50document, and do not invent a tool you did not find via `tool_search` or the
51catalog.
52
53## Document shape
54
55```json
56{
57 "id": "research-brief",
58 "name": "Research brief",
59 "inputs": [{ "name": "topic", "type": "text", "required": true }],
60 "nodes": [
61 { "id": "search", "type": "tool.web_search",
62 "inputs": { "query": { "from": "in.topic" }, "max_results": { "value": 5 } } },
63 { "id": "summary", "type": "llm.generate",
64 "inputs": { "instruction": { "value": "Summarize as 5 bullets." },
65 "input": { "from": "search.results_text" } } }
66 ],
67 "outputs": { "brief": { "from": "summary.text" } }
68}
69```
70
71- `id` matches `^[a-zA-Z0-9_-]+$`; `name` is human-readable.
72- `inputs` are the workflow's typed parameters, exposed to nodes as
73 `in.<name>` (e.g. `{"from": "in.topic"}`).
74- Each node has a unique `id`, a `type` from the catalog, and an `inputs` map
75 keyed by the manifest's input port names.
76- `outputs` maps result names to node output ports; this is the run result.
77- `in` and `item` are reserved node ids.
78- Optional: `settings.max_concurrency` (default 4); `publish.tool: true` to
79 expose the workflow as an agent tool; `project_id` to scope file tools to a
80 project.
81
82## Types and coercions
83
84Port types: `text`, `number`, `boolean`, `json`, `file`, `list<T>` (T is any
85scalar). The **only** implicit coercions, applied to wires, are:
86
87- `number → text`
88- `boolean → text`
89
90Nothing else coerces. `json → text` needs `data.stringify`; `text → json` needs
91`data.parse`; pull a field out of `json` with `data.extract`. **Literals must
92match the port type exactly** (`"5"` for a text port, `5` for a number port). A
93`file` port accepts a literal path string.
94
95## Builtin nodes (stable; always available)
96
97| Type | Inputs | Outputs |
98|---|---|---|
99| `data.extract` | `source: json`, `path: text`, `as: text` (default `"json"`) | `value` (typed by `as`) |
100| `data.template` | `template: text`, `vars` (named, any type) | `text: text` |
101| `data.merge` | `objects` (list of `json`) | `json: json` |
102| `data.stringify` | `value: T` | `text: text` |
103| `data.parse` | `text: text` | `json: json` |
104| `data.constant` | `value: json`, `as: text` (default `"json"`) | `value` (typed by `as`) |
105| `logic.if` | `condition: boolean`, `value: T` | `then: T`, `else: T` (one fires) |
106| `logic.coalesce` | `values` (list of `T`) | `value: T` (first that fired) |
107| `logic.equals` | `a: T`, `b: T` | `result: boolean` |
108| `logic.contains` | `haystack: text`, `needle: text` | `result: boolean` |
109| `logic.not` | `value: boolean` | `result: boolean` |
110| `logic.greater_than` | `a: number`, `b: number` | `result: boolean` |
111| `control.map` | `items: list<T>`, `concurrency: number`, `on_item_error: text` | `results: list<…>` |
112| `llm.generate` | `instruction: text`, `input: text`, `model: text` | `text: text` |
113
114**Tool nodes** are `tool.<name>` — one for every tool in the registry,
115including MCP-server tools (e.g. `tool.web_search`, `tool.file_write`,
116`tool.brave_brave_web_search`). The set is instance-specific and not known in
117advance, so **find tools with `tool_search`** (by capability) rather than
118assuming; then use the match as node type `tool.<its name>`. Each tool node has
119a `result: json` output plus, for curated tools, typed outputs. Get the exact
120input/output ports from `workflow_catalog`.
121
122`data.extract` / `data.constant` set their output type via the `as` config:
123one of `text`, `number`, `boolean`, `json`, `list<json>`.
124
125## Named-variadic and list-variadic ports
126
127- `data.template.vars` is **named-variadic**: an object of bindings, one per
128 `{placeholder}` in the template —
129 `"vars": { "topic": {"from":"in.topic"}, "n": {"value": 3} }`.
130- `logic.coalesce.values` and `data.merge.objects` are **list-variadic**: an
131 array of bindings — `"values": [ {"from":"a.text"}, {"from":"b.text"} ]`.
132
133## Control flow (gated wires, not conditionals)
134
135- `logic.if` fires **exactly one** of `then` / `else` per run based on
136 `condition`.
137- A node whose **required** input never fires (e.g. it reads the branch that
138 didn't fire) is **skipped**, and skips cascade downstream. An **optional**
139 input whose source is skipped falls back to its default.
140- `logic.coalesce` rejoins branches: it emits the first of its inputs that
141 actually fired.
142- `control.map` fans out over `items: list<T>`: its `body` subgraph runs once
143 per item, seeing `item.value` and `item.index`, and gathers the body's single
144 `result` output into `results`. `on_item_error` is `fail` (default) or
145 `skip`. Bodies may also wire from outer / `in` ports.
146
147## Common mistakes to avoid
148
149- Assuming a tool exists or guessing its name. Use `tool_search` to find the
150 right tool (MCP servers add tools you can't know in advance), then read its
151 exact ports from `workflow_catalog`.
152- Using an expression / template string in a binding. Use a `data.*` node.
153- Wiring `json` into a `text` port (or vice-versa) without a
154 `data.stringify` / `data.parse` / `data.extract` between them.
155- A literal whose JSON type doesn't match the port (e.g. `5` into a text port).
156- Forgetting `data.template.vars` is a named object and
157 `logic.coalesce.values` is an array.
158- Returning a document without running it through `workflow_save`.
159
160## More examples
161
162Read `references/examples.md` (via `skill_read`, path `references/examples.md`)
163for full worked documents: a linear pipeline, an if/coalesce branch, and a map
164fan-out.