Workflows
A workflow chains several apps into one flow: a trigger, then the apps that run one after another, with conditions that branch on what the previous app answered. The API is the authoring surface — you create the flow, draw or describe its steps, read back whether it can actually run, activate it and publish it. It is the same object the workspace draws on its canvas, so a flow written here opens in the flow editor and a flow drawn there is readable here.
Endpoints
| Method | Path | Scope |
|---|---|---|
| POST | /v1/workflow/create | workflow:write |
| GET | /v1/workflow/list | workflow:read |
| GET | /v1/workflow/detail/<workflow_id> | workflow:read |
| PATCH | /v1/workflow/detail/<workflow_id> | workflow:write |
| DELETE | /v1/workflow/detail/<workflow_id> | workflow:write |
<workflow_id> is a workflow identifier, prefixed wkfl_.
Important — There is no endpoint that launches a workflow. A run is advanced by the workspace, one step per page load, so a run created from outside it would stay at
queuedand never move. Launch a flow from its page in the workspace. To run a single app from the API, use Runs instead.
The workflow object
| Field | Type | Description |
|---|---|---|
id | string | The workflow identifier, prefixed wkfl_. |
object | string | Always workflow. |
name | string | The flow's name. |
description | string | What the flow does. |
type | string | private or public. public means the flow is offered in the catalogue. |
status | string | draft or activated. Only an activated flow can be launched. |
trigger | string | What starts the flow. Always manual today. |
counts | object | app and condition: how many of each the drawing holds. |
apps | array | One { "id", "name" } entry per distinct app the flow chains, in drawing order. |
graph | object | The drawing: nodes and edges. Detail and write responses only. |
ready | boolean | true when the flow validates and can run. Detail and write responses only. |
issues | array | One sentence per rule the drawing breaks. Empty when ready is true. Detail and write responses only. |
runs | object | total, private (this project's own) and public (made through the published copy). |
created_at | integer | When the flow was created, in Unix seconds. |
updated_at | integer | Last change, in Unix seconds. |
The graph
The drawing is a set of nodes joined by edges.
| Node field | Type | Description |
|---|---|---|
key | string | The node's own name in the drawing, 1 to 40 characters from A-Z a-z 0-9 _ -. |
type | string | trigger, app or condition. |
x, y | integer | Where the box sits on the canvas. Optional — a node without coordinates is laid out on the grid. |
config | object | Depends on the type, see below. |
| Node type | config |
|---|---|
trigger | event, which is manual. Anything else reads as manual. |
app | app_ident, the app the step runs, and optionally fields (see below). |
condition | operator and value. Operators are contains, not_contains, equals, not_empty. |
| Edge field | Type | Description |
|---|---|---|
from | string | The node key the arrow leaves. |
to | string | The node key the arrow enters. |
branch | string | Empty for a plain arrow. On a condition, true or false says which way the arrow leaves. |
A flow is ready to run when it has exactly one trigger, at least one app, every app step names an activated app of this project with a usable model, every required field is fed, no condition sits first in the chain, no arrow closes a loop, and every node is reachable from the trigger. When a rule is broken, issues names it — a node-scoped rule reads Step 'n2': ….
A drawing holds at most 60 nodes and 7200 edges.
Note — Anything the canvas would not draw is dropped, not refused: an unknown node type, a coordinate off the grid, an arrow to a node that does not exist, an arrow from a node to itself, a duplicated arrow. The
graphin the response is the drawing as it was stored, so read it back to see exactly what was kept.
Pinning a step's fields
By default an app step is fed by the step above it. config.fields overrides that, one entry per field, keyed by the field's name or by its madi_ identifier.
| Field kind | Shape | Sources |
|---|---|---|
| Text | { "source", "value" } — source is previous, input or static | previous takes the answer of the step above, input asks the person who launches the flow, static always sends value. |
| Document or image | { "source", "file" } — source is input or static, and file is a file_… identifier | input asks for a file at launch, static always sends the one named. |
A bare value is shorthand for static: "tone": "formal" is the same as "tone": { "source": "static", "value": "formal" }. An empty object takes the default — previous for text, input for a file.
Create a workflow
POST /v1/workflow/create takes either a drawing or an ordered list of steps.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | The flow's name, at most 190 characters. |
description | string | yes | What the flow does. |
graph | object | no | A drawing, as described above. Also accepted as a JSON string. |
steps | array | no | An ordered list of steps, wired for you. At most 59. |
status | string | no | draft or activated. Defaults to activated. |
type | string | no | private or public. Defaults to private. |
Send graph when you already have a drawing, steps when you just want the apps to run in order. Each step is { "app": "mapp_… or a name", "fields": {…} } or { "condition": { "operator": "contains", "value": "urgent" } }. Steps are wired to each other in the order you send them, a trigger is minted in front, and a condition passes to the next step on its true branch. Add "on_false" to a condition, naming another step by its number, to wire the other branch.
curl -X POST https://api.thinkhx.com/v1/workflow/create \
-H "Authorization: Bearer sk-project-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Triage then answer",
"description": "Sort the incoming message, then draft the reply.",
"steps": [
{ "app": "mapp_0ceacb46e5b3a99e91b6" },
{ "condition": { "operator": "contains", "value": "urgent" } },
{ "app": "mapp_7d3f81b0c25e94a6f01d" }
]
}' {
"code": 200,
"title": "OK",
"response": {
"id": "wkfl_4a91c7e0d25b83f6a1cd",
"object": "workflow",
"name": "Triage then answer",
"description": "Sort the incoming message, then draft the reply.",
"type": "private",
"status": "activated",
"trigger": "manual",
"counts": { "app": 2, "condition": 1 },
"apps": [
{ "id": "mapp_0ceacb46e5b3a99e91b6", "name": "Message triage" },
{ "id": "mapp_7d3f81b0c25e94a6f01d", "name": "Reply drafter" }
],
"graph": {
"nodes": [
{ "key": "n1", "type": "trigger", "x": 60, "y": 220, "config": {} },
{ "key": "n2", "type": "app", "x": 360, "y": 220, "config": { "app_ident": "mapp_0ceacb46e5b3a99e91b6" } },
{ "key": "n3", "type": "condition", "x": 660, "y": 220, "config": { "operator": "contains", "value": "urgent" } },
{ "key": "n4", "type": "app", "x": 960, "y": 220, "config": { "app_ident": "mapp_7d3f81b0c25e94a6f01d" } }
],
"edges": [
{ "from": "n1", "to": "n2", "branch": "" },
{ "from": "n2", "to": "n3", "branch": "" },
{ "from": "n3", "to": "n4", "branch": "true" }
]
},
"ready": true,
"note": null,
"created_at": 1754209200
}
} Creation is the one place an invalid flow is refused: the drawing does not exist yet, so there is nothing to lose by asking for it again.
| Code | Message | Cause |
|---|---|---|
| 400 | Missing required field: name. | No name was sent. |
| 400 | Missing required field: description. | No description was sent. |
| 422 | The graph could not be read: send it as a JSON object (or a JSON string) carrying 'nodes' and 'edges'. | graph was not a readable object. |
| 422 | The flow is not valid: <issues> | The drawing breaks one or more of the rules above. |
| 422 | steps must be a non-empty ordered array of steps. | steps was empty or was not a list. |
| 422 | steps must contain at most 59 steps. | Too many steps for the canvas. |
| 422 | Step <n> names an app that does not exist, is not activated, or is not available to this project: name one of this project's activated apps by id (mapp_…) or by name. | The step names an unusable app. |
| 422 | Step <n> uses an unknown condition operator: accepted operators are contains, not_contains, equals, not_empty. | The operator is not one of the four. |
| 422 | status must be 'draft' or 'activated'. | An unknown status. |
| 422 | type must be 'public' or 'private'. | An unknown type. |
| 402 | The current billing plan does not allow workflow creation for this project. | The plan carries no workflow allowance. |
| 402 | The current billing plan workflow limit has been reached for this project. | The project already holds as many flows as the plan allows. |
Sending both graph and steps is not an error: the drawing wins, and the response says so in note: Both 'graph' and 'steps' were provided: 'graph' was used and 'steps' ignored.
Note — A step can only name an app of your own project, even a published one of another project: the flow editor cannot draw a step over an app it does not own, so the step would be wiped the first time the flow is opened. Copy the app into your project first.
List your workflows
GET /v1/workflow/list returns the project's flows, newest first. Deleted flows are excluded.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Between 1 and 100. Defaults to 50. |
offset | integer | How many rows to skip. Defaults to 0. |
curl "https://api.thinkhx.com/v1/workflow/list?limit=2" \
-H "Authorization: Bearer sk-project-YOUR_KEY" {
"code": 200,
"title": "OK",
"response": {
"object": "list",
"data": [
{
"id": "wkfl_4a91c7e0d25b83f6a1cd",
"object": "workflow",
"name": "Triage then answer",
"description": "Sort the incoming message, then draft the reply.",
"type": "private",
"status": "activated",
"trigger": "manual",
"counts": { "app": 2, "condition": 1 },
"apps": [
{ "id": "mapp_0ceacb46e5b3a99e91b6", "name": "Message triage" },
{ "id": "mapp_7d3f81b0c25e94a6f01d", "name": "Reply drafter" }
],
"runs": { "total": 12, "private": 12, "public": 0 },
"created_at": 1754209200,
"updated_at": 1754212800
}
],
"total": 7
}
} A listed row carries no graph, no ready and no issues: a page of drawings is a large answer, and validity costs a handful of queries per flow. Read one flow to get all three.
Read one workflow
GET /v1/workflow/detail/<workflow_id> returns the whole flow, drawing included.
curl https://api.thinkhx.com/v1/workflow/detail/wkfl_4a91c7e0d25b83f6a1cd \
-H "Authorization: Bearer sk-project-YOUR_KEY" The graph you read back is the normalised drawing — what the canvas and the engine actually read, with anything unrecognised already dropped. A flow of another project gives 404 and Workflow not found., whether it exists or not.
Edit a workflow
PATCH /v1/workflow/detail/<workflow_id> changes only the fields present in the body.
| Parameter | Type | Description |
|---|---|---|
name | string | A new name. |
description | string | A new description. |
graph | object | A new drawing. Replaces the old one. |
status | string | draft or activated. |
type | string | private or public. |
curl -X PATCH https://api.thinkhx.com/v1/workflow/detail/wkfl_4a91c7e0d25b83f6a1cd \
-H "Authorization: Bearer sk-project-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Triage, then answer" }' Important — A PATCH carrying a
graphalways saves the drawing, even when the flow does not validate. It is not refused: the flow is put back todraftin the same write and the answer is still200, with"ready": false, the failing rules underissues, andnotereadingThe graph was saved but does not validate, so the workflow was set back to draft.This is what the canvas does — a half-drawn flow is saved so you can come back to it. So after a PATCH, readreadyandissues, never the HTTP status, to know whether the flow can run.
Activating and publishing are the gates. "status": "activated" and "type": "public" are both refused when the final drawing does not validate, and publishing a flow also requires that it is activated and that every app it chains is itself published.
| Code | Message | Cause |
|---|---|---|
| 404 | Workflow not found. | Unknown, deleted, or another project's flow. |
| 422 | Missing required field: name. | name was present but empty. |
| 422 | Missing required field: description. | description was present but empty. |
| 422 | The flow is not valid: <issues> | An activation or a publication was asked for on a drawing that does not validate. |
| 422 | The workflow must be activated before it can be published. | type: public on a draft. |
| 422 | Every app this workflow chains must be published before the workflow can be published: '<name>' is not. | A chained app is still private. |
A field mapping that names no field of its app is a 422 when you send it through steps, but inside a hand-drawn graph it is dropped and reported in note — the drawing is kept either way.
Delete a workflow
DELETE /v1/workflow/detail/<workflow_id> removes the flow from the listings, from the catalogue if it was published, and from the workspace.
curl -X DELETE https://api.thinkhx.com/v1/workflow/detail/wkfl_4a91c7e0d25b83f6a1cd \
-H "Authorization: Bearer sk-project-YOUR_KEY" {
"code": 200,
"title": "OK",
"response": { "deleted": true, "id": "wkfl_4a91c7e0d25b83f6a1cd" }
} The delete is a soft one: the runs the flow already produced are kept and stay readable. Deleting a flow that is already gone answers 404 and Workflow not found.