Workflows & the interface
A workflow is the thing you run. This document explains how to find the workflows available to you, and — more importantly — how to read a workflow’s interface: the contract that tells you exactly what to send in and what you will get back. The interface is the single most important concept in the whole API, so most of this page is about it.
1. Listing workflows
Section titled “1. Listing workflows”GET {BASE_URL}/api/v1/workflowsReturns by default the published workflows visible to your workspace. With the authoring API
(10-authoring.md) you can also list drafts and archived workflows via the
status filter; the default stays published so existing integrations see no change.
| Query param | Type | Default | Description |
|---|---|---|---|
limit |
integer | 25 | Items per page (1–100). |
starting_after |
string | – | Pagination cursor: the next_cursor from the previous page. |
tag |
string | – | Return only workflows carrying this tag. |
status |
string | published |
draft, published, archived or all. |
query |
string | – | Search every workflow of the workspace, in every folder: a workflow ID (wf_…) finds that workflow; any other text the workflows whose name, description or a tag contains it, ignoring case. MCP search_workflows reads its query the same way. |
curl -s -H "Authorization: Bearer $TOKEN" \ "$BASE_URL/api/v1/workflows?limit=10&tag=newsletter"Response (the standard paginated envelope — see README §6):
{ "data": [ { "id": "wf_53fb2f6576fa4bc9a6d3c91a7e84de47", "name": "Newsletter Hero", "description": "Generates a hero image + marketing copy from product photos.", "version": 4, "status": "published", "tags": ["newsletter", "ecommerce"], "created_at": "2026-05-01T09:12:00+00:00", "updated_at": "2026-05-20T16:40:00+00:00" // note: the list view does NOT include the interface — fetch the detail for that } ], "has_more": false, "total_count": 1}The list view is a catalogue: it gives you names, descriptions, tags, and the current
version, but not the interface. To learn a workflow’s inputs/outputs you fetch its detail.
Folders
Section titled “Folders”Workflows live in the workspace’s folder tree, as in the editor. Folders have public IDs fld_…:
GET /api/v1/workflow-folders # every folder: id, name, parent_id, path, countsGET /api/v1/workflow-folders/{fld_…|root}/content # its subfolders and workflows (optional ?search=)POST /api/v1/workflow-folders # {"name":"Examples","parent_id":"fld_…"}POST /api/v1/workflow-folders/move-workflows # {"workflow_ids":["wf_…"],"folder_id":"fld_…"} (null = root)Reading needs workflows:read, creating and moving workflows:write. A move is all or nothing:
an unknown workflow moves none. MCP (get_workflow_folder with view tree or content,
organize_workflow_folders with operation create or move) and the in-app agent (workflow.list_folders,
workflow.get_folder, workflow.create_folder, workflow.move_to_folder) run the same operations —
for example to open a folder of example workflows and read their definitions before building a new one.
2. Getting a workflow (with its interface)
Section titled “2. Getting a workflow (with its interface)”GET {BASE_URL}/api/v1/workflows/{id}This is the call you make before every integration. It returns the workflow plus its full interface.
| Query param | Type | Description |
|---|---|---|
version |
integer | (Optional) Inspect a specific historical version’s interface instead of the latest published one. See §7 Versioning. |
curl -s -H "Authorization: Bearer $TOKEN" "$BASE_URL/api/v1/workflows/$WF"{ "id": "wf_53fb2f6576fa4bc9a6d3c91a7e84de47", "name": "Newsletter Hero", "version": 4, "status": "published", "tags": ["newsletter"], "created_at": "2026-05-01T09:12:00+00:00", "interface": { /* the default interface — see §3 */ }, "interfaces": [ /* custom interfaces, if any — see §5 */ ], "execution_contract": { /* how to pass inputs (§2.1 of 04-executions) + outputs: where to read results — see §6 */ }}If the ID is malformed you get 400 (invalid_id); if the workflow does not exist in your
workspace you get 404 (not_found). A key with the read permission resolves workflows in
any status — drafts and archived included; check the status field before executing
(executions require a published workflow). Status transitions — publish, archive, revert, clone —
are driven through the authoring API (10-authoring.md §7).
3. The default interface
Section titled “3. The default interface”Every workflow has a default interface, found in the interface field. Madoo generates it
automatically from the input nodes and output nodes the author placed on the canvas. It is
the plain, complete description of the workflow’s I/O.
"interface": { "inputs": [ { "name": "block_title", // ← the key you use in execution inputs "type": "text", // ← data type "label": "Block Title", // ← human-readable label (the source of the name) "required": true, // ← must you provide it? "default_value": null // ← JSON-encoded default, if the author set one }, { "name": "product_image_0", "type": "image", "label": "Product Image 0", "required": true }, { "name": "product_image_1", "type": "image", "label": "Product Image 1", "required": false } ], "outputs": [ { "name": "hero_image", "type": "image", "label": "Hero Image" }, { "name": "copy_json", "type": "text", "label": "Copy JSON" } ]}Input/output port fields
Section titled “Input/output port fields”Each port (an entry in inputs or outputs) has:
| Field | Meaning |
|---|---|
name |
The key you use when sending inputs / matching outputs. |
type |
Data type. Inputs: text, number, boolean, image, video, json, any. Outputs: image, video, text, json. |
label |
Human-readable label set by the author. |
required |
(inputs) Whether you must provide a value (see §4). |
default_value |
(inputs) A JSON-encoded default value, if the author defined one; otherwise omitted. |
Where do the name keys come from? (The naming rule)
Section titled “Where do the name keys come from? (The naming rule)”The port name is derived from the node’s label, by a deterministic rule:
name= the node’s label, trimmed, with spaces replaced by underscores, lower-cased. If two nodes would produce the same key, the later ones get a numeric suffix (_2,_3, …).
Examples:
| Node label | Port name |
|---|---|
Block Title |
block_title |
Product Image 0 |
product_image_0 |
Brand URL |
brand_url |
Title (first) / Title (second) |
title / title_2 |
Output keys follow the same idea with one more source and a stricter cleanup: an output node’s
output_name parameter, when the author set one, wins over the label; otherwise the label is used,
and with neither the node type. The text is lower-cased and every character that is not a letter or a
digit becomes _ (repeated _ collapse, leading/trailing _ are dropped), so a label Product Cards
or an output_name of product-cards both give product_cards. Duplicates get _2, _3, …
You do not need to compute this yourself — the API gives you the final name in the interface.
The rule is documented only so the keys are not mysterious. The practical takeaway is unchanged:
read the interface and use the name values verbatim. Do not hard-code or guess them, because if
the author renames a node, the key changes in the next version.
Input node types
Section titled “Input node types”Behind each input port is an input node of one of these types:
| Node type | Port type |
You send it as |
|---|---|---|
input/text |
text |
value (JSON-encoded string/number/bool/JSON) |
input/image |
image |
asset_path, or a JSON-encoded public HTTPS URL in value |
input/video |
video |
asset_path, or a JSON-encoded public HTTPS URL in value |
input/audio |
audio* |
asset_path, or a JSON-encoded public HTTPS URL in value |
input/document |
document* |
asset_path, or a JSON-encoded public HTTPS URL in value |
input/model3d |
model3d* |
asset_path, or a JSON-encoded public HTTPS URL in value |
input/batch |
– | Used by batch executions (06-advanced-batch.md) |
*The exact type string is whatever the interface reports for that port — always trust the
interface over this table.
Public media URLs are execution-time inputs: Madoo securely downloads and validates them, then gives
all compatible nodes the same temporary internal asset. Use asset_path for an uploaded/imported
asset, especially for private content or repeated use. Authenticated remote URLs are not supported.
When the URL is produced inside the workflow as text or url—for example by JSON extraction,
document analysis, or a future web-data node—add utility/media_from_url. Set its step-shape
media_type to image, video, audio, document, or model3d, then connect its dynamic media
output to compatible nodes. The node creates execution-temporary media; it does not import a
permanent library asset. If the upstream output is already media-typed, no conversion node is needed.
4. Required vs optional inputs
Section titled “4. Required vs optional inputs”Each input declares required. The rule is simple:
required: true— you must include this input, or the execution is rejected with a validation error.required: false— you may omit the input entirely. The workflow runs anyway; the corresponding input node simply emits a null value, and the workflow is designed to tolerate the gap.
This is what lets a workflow accept, say, up to six product images while needing only one:
product_image_0 is required: true, and product_image_1 … product_image_5 are
required: false. Send one image, or send several with holes in the sequence
(product_image_0, product_image_1, product_image_3) — both work.
Default for older workflows. If a workflow was created before optional inputs existed, every input reports
required: true. There is never an input that is silently optional — trust the flag.
5. Custom interfaces
Section titled “5. Custom interfaces”The default interface exposes everything. Often that is more than an integrator should care
about, or the keys (product_image_0) are less friendly than you’d like. So a workflow author can
publish one or more custom interfaces: curated, named views over the same workflow.
A custom interface can:
- expose only a subset of inputs, with friendlier field keys and labels;
- attach constraints (min/max/step for numbers, an allowed-values list for enums);
- supply default values;
- restrict which outputs are returned;
- set a node parameter on every run (a crop box, a strength), by mapping a field to it.
To write one — the mapping of each field and output, with a complete example that maps fields to the
parameters of image/crop — see 10-authoring.md §1.6.
Custom interfaces appear in the interfaces array of the workflow detail (omitted when there are
none):
"interfaces": [ { "id": "newsletter-simple", // ← use this in the execution's "interface" field "name": "Simple Newsletter", "description": "Just a title and one product photo.", "is_default": true, // ← author's recommended interface (the execution_contract primary); still pass its id to use it "fields": [ { "key": "title", // ← the input key for THIS interface "label": "Headline", "type": "text", "required": true, "default_value": "\"Spring in Bloom\"" }, { "key": "quality", "label": "Quality", "type": "number", "required": false, "default_value": "2", "constraints": { "min": 1, "max": 5, "step": 1 } }, { "key": "style", "label": "Style preset", "type": "enum", "required": false, "constraints": { "allowed_values": ["editorial", "minimal", "vibrant"] } }, { "key": "photo", "label": "Product photo", "type": "image", "required": true } ], "outputs": [ { "key": "image", "label": "Hero image" } ] }]Field vs port — what’s the difference?
Section titled “Field vs port — what’s the difference?”The default interface lists inputs/outputs as ports (name, type, required,
default_value). A custom interface lists fields/outputs as fields, which are richer:
| Field property | Meaning |
|---|---|
key |
The input key to use for this interface (may differ from the default port name). |
label, description |
Human-readable. |
type |
text, number, boolean, image, enum, preset, effect. |
required |
Whether you must provide it. |
default_value |
JSON-encoded default, if any. |
category |
For preset fields: the preset category code. |
constraints |
Optional: min, max, step (numbers), allowed_values (enums). |
For
presetandeffectfields, the valid values are codes from the public catalog — list them withGET /api/v1/presets?category=…andGET /api/v1/effects. See 09-catalog.md.
Executing through a custom interface
Section titled “Executing through a custom interface”Pass the interface’s id in the execution request’s interface field, and key your inputs by
the interface’s field keys (not the default port names):
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{ "workflow": "'"$WF"'", "interface": "newsletter-simple", "inputs": { "title": { "value": "\"Spring in Bloom\"" }, "photo": { "asset_path": "'"$ASSET_PATH"'" } }}' "$BASE_URL/api/v1/executions"Behaviour worth knowing:
is_defaultmarks the recommended interface, but does NOT auto-apply. A custom interface markedis_default: trueis surfaced as theprimaryin the workflow’sexecution_contract, but execution only runs through a custom interface when you explicitly pass itsidin theinterfacefield. Omittinginterfacealways uses the raw auto-generated default interface (the default port names), never a custom one — so whenexecution_contract.primary.interfaceis present, pass it.- An unknown interface ID is rejected with a validation error that lists the available interface IDs.
- Outputs are filtered to those the interface declares — a custom interface that exposes only
imagewill not return the workflow’s other outputs.
Rule of thumb: if a workflow publishes a custom interface aimed at your use case, prefer it — it is the author’s stable, curated contract. Use the default interface when you need full access to every input/output.
6. execution_contract.outputs — where to read results
Section titled “6. execution_contract.outputs — where to read results”GET /api/v1/workflows/{id} also returns an execution_contract. Its primary block tells you
how to pass inputs (04-executions §2.1);
its outputs block tells you how to read results — before you ever run the workflow. Each
entry carries the exact read_from path the /result surface
serves, so you can write the read side of your integration up front.
"execution_contract": { "primary": { /* inputs … */ }, "outputs": [ { "key": "newsletter_copy", "type": "text", "label": "Newsletter copy", "required": false, "cardinality": "single", "read_from": "result.by_key.newsletter_copy.value", // ← read here "delivery": "resolved", // resolved (text/JSON) | asset (binary) "source_node_id": "out_copy", "logical_name": "newsletter_copy", "presence": ["present", "absent"], "schema": null, "notes": ["This output may be JSON-in-text. The result surface parses it at runtime; read value, not url."] }, { "key": "hero_image", "type": "image", "read_from": "result.by_key.hero_image.url", // ← binary → read .url "delivery": "asset", "cardinality": "single", "logical_name": "hero_image", "presence": ["present", "absent"], "schema": null } ], "result_example": { "result": { "by_key": { "newsletter_copy": { "read_from": "result.by_key.newsletter_copy.value" } } } }}| Field | Meaning |
|---|---|
key |
The key to look up under result.by_key. |
type |
Physical output type (text, image, json, …). For an output/text node carrying JSON the type is honestly text — /result discovers and parses the JSON at runtime; the contract never promises a structured shape it cannot guarantee. |
read_from |
The exact path to read, starting from the root of the /result response body: that body has a result field, so result.by_key.x.value means body.result.by_key.x.value — not body.by_key…. ….value for resolved text/JSON, ….url for binary. For a multiple output it is result.by_key.x.items[*].value (or .url): read every element of items. |
delivery |
resolved (read inline via value) or asset (binary, read via url). |
cardinality |
single or multiple. multiple when the output node sits downstream of an enumerator with no aggregator in between (one item per iteration), or when its name is set dynamically. A run that happens to produce one item still fills items[0], so items[*] is always safe to read. |
presence |
The states the output can take, e.g. ["present","absent"]. |
schema |
Reserved for a future structured-output JSON schema; null today. |
Custom interfaces: every interface carries its own
outputsandresult_example— the primary one and each entry ofalternative_interfaces[]— and/resultgroups a run by the keys of the interface the run was started with. The top-leveloutputs/result_examplerepeat the primary interface’s. A custom interface that declares no outputs has nooutputsblock (null), so read the one of the interface you run.
Besides outputs and result_example, the contract carries:
| Field | Meaning |
|---|---|
primary |
The interface to run: interface (id or null), input_key_mode, inputs[], a ready-to-copy example, and its own outputs / result_example (the top-level ones repeat these). |
accepted_aliases |
A map from each input key to the other names that bind to it ({"brief": ["in_brief"]}) — only on the auto default. |
alternative_interfaces |
The other custom interfaces, each with the same shape as primary. |
notes |
One string with the binding rules in plain words (how to pass values and files). |
The contract has two shapes, depending on whether the author marked a custom interface as default:
| Auto default (no custom default) | Custom interface marked default | |
|---|---|---|
primary.input_key_mode |
default_field_key — keys from the labels; the input node id also binds |
interface_field_key — only the interface’s field keys bind |
primary.interface |
absent/null — send no interface |
the interface id — send it as interface |
accepted_aliases |
node id per key ({"brief": ["in_brief"]}) |
absent — node ids do not bind |
alternative_interfaces |
every custom interface, if any | the other custom interfaces, if any |
outputs / result_example |
keys from output_name / label |
the interface’s own output keys (null when it declares none) |
7. Versioning
Section titled “7. Versioning”Workflows are versioned. The version field in the workflow detail is the latest published
version. By default, executions run against the latest version — which means an author publishing
a new version can change the interface under you.
To insulate your integration from that, you can pin a version:
- List the existing versions:
GET /api/v1/workflows/{id}/versions(newest first, with anis_currentflag). - Inspect a specific version’s interface:
GET /api/v1/workflows/{id}?version=3 - Execute against a specific version: include
"version": 3in the execution request (04-executions.md).
Requesting a version below 1 or above the latest returns 400 (invalid_version) with the valid
range; a version whose definition is no longer available returns 404 (version_not_available).
Recommendation: for stable, long-lived integrations, pin the version you tested against, and bump it deliberately after re-checking the interface. For always-take-the-latest behaviour, omit
version— but re-read the interface periodically.
Next: 04-executions.md — submitting executions, the input-encoding rules in full, polling, and reading outputs. To build workflows through the API instead of the editor, see 10-authoring.md.