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. |
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 |
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.
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 on the result: ….value for resolved text/JSON, ….url for binary. |
delivery |
resolved (read inline via value) or asset (binary, read via url). |
cardinality |
single or multiple (iterated). |
presence |
The states the output can take, e.g. ["present","absent"]. |
schema |
Reserved for a future structured-output JSON schema; null today. |
Custom interfaces:
execution_contract.outputsadvertises the workflow’s default/runtime output keys — the ones/resultserves today. Custom-interface output renaming is a separate, upcoming capability; until it ships, read by the runtime keys shown here.
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.