Skip to content

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.


GET {BASE_URL}/api/v1/workflows

Returns 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.
Terminal window
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.

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, counts
GET /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.
Terminal window
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).


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" }
]
}

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.

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.


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.


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 preset and effect fields, the valid values are codes from the public catalog — list them with GET /api/v1/presets?category=… and GET /api/v1/effects. See 09-catalog.md.

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):

Terminal window
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_default marks the recommended interface, but does NOT auto-apply. A custom interface marked is_default: true is surfaced as the primary in the workflow’s execution_contract, but execution only runs through a custom interface when you explicitly pass its id in the interface field. Omitting interface always uses the raw auto-generated default interface (the default port names), never a custom one — so when execution_contract.primary.interface is 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 image will 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.outputs advertises the workflow’s default/runtime output keys — the ones /result serves today. Custom-interface output renaming is a separate, upcoming capability; until it ships, read by the runtime keys shown here.


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 an is_current flag).
  • Inspect a specific version’s interface: GET /api/v1/workflows/{id}?version=3
  • Execute against a specific version: include "version": 3 in 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.