Flows, nodes, edges
A flow is a graph. Nodes do work; edges connect them. Node ids are human-readable (node1, node2, …) and visible on the canvas — keep them stable. One edge carries both execution order and data: the source's output items flow into the target. Every node with no incoming edge is a start node; output ("Respond") nodes are terminal.
The node types available today (always confirm with GET /node-types, which is the source of truth for config fields):
| Type | Does |
|---|---|
trigger | Manual start. Emits its config.sampleData on a run. |
whatsappTrigger | Starts the flow when a WhatsApp account connected in your workspace receives a matching message (only while the flow is active). |
llmChain | Runs a prompt through a model, once per input item. Output is { text } or the JSON fields you define. |
httpRequest | Calls a URL; a JSON response becomes the item. |
condition (If) | Routes each item to a true or false branch. |
sendWhatsapp | Sends one WhatsApp message per item — a reply to the triggering message by default. |
output | Terminal; whatever reaches it is the flow's result. |
action | Placeholder pass-through. |
Rules the API enforces: no self-loops, no duplicate edge between the same pins, triggers have no input pin, output has no output pin, unknown node types are rejected. A PUT …/graph or POST /flows is atomic — if anything is invalid, nothing is saved.
Items: how data moves
Data travels as an array of items; each item is { "json": { …fields } }. A node receives items and emits items:
| Node | Emits |
|---|---|
trigger | Its sampleData: an object → 1 item; an array → one item per element. |
llmChain | Replaces the item with { text } (or your JSON fields) plus _llm: { model, inputTokens, outputTokens, credits }. Incoming fields are not carried through. |
httpRequest | One item: the parsed JSON body, or { body, status } for other content types. Non-2xx is a node error unless ignoreResponseCode. |
condition | Each item to one branch, with _if: { result, matched[] } added. |
sendWhatsapp | The input item plus _whatsapp: { sent, messageId, account, to, body }. |
A branch that receives zero items does not run its downstream nodes.
Merge fields
Any string in a node's config — prompts, URLs, condition operands — can reference data:
| Syntax | Resolves to |
|---|---|
{{field}}, {{a.b.c}} | A value from the incoming item (the direct parent's output). |
{{$json}} | The whole incoming item as JSON. |
{{[node1].field}} | A field from another node's output (its first item). |
The usual mistake
After an LLM node the incoming item is { text, _llm }, so {{name}} is empty. Reach back with {{[node1].name}}.
Branching with If
A condition node evaluates config.conditions[] ({ left, operator, right }, joined by combinator) per item. Edges leaving it must say which branch:sourceHandle: "true" | "false". Numbers, booleans and dates compare by value; unary operators (exists, is_empty, …) ignore right. The operator list is in GET /node-types.
{
"nodes": [
{ "id": "node1", "type": "trigger", "config": { "sampleData": { "amount": 250 } } },
{ "id": "node2", "type": "condition", "label": "amount > 100?",
"config": { "conditions": [ { "left": "{{amount}}", "operator": "greater_than", "right": "100" } ], "combinator": "and" } },
{ "id": "node3", "type": "llmChain", "label": "VIP reply", "config": { "prompt": "Write a one-line thank-you for a VIP spending {{[node1].amount}}." } },
{ "id": "node4", "type": "llmChain", "label": "Basic reply", "config": { "prompt": "Write a one-line hello." } },
{ "id": "node5", "type": "output" }
],
"edges": [
{ "source": "node1", "target": "node2" },
{ "source": "node2", "target": "node3", "sourceHandle": "true" },
{ "source": "node2", "target": "node4", "sourceHandle": "false" },
{ "source": "node3", "target": "node5" },
{ "source": "node4", "target": "node5" }
]
}Building a flow
Create a flow with its whole graph in one call, or start empty and add nodes:
curl -X POST https://api.simplynice.ai/api/ai/simple-flow/flows -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Sentence → Chinese → Japanese",
"nodes": [
{ "id": "node1", "type": "trigger", "config": { "sampleData": { "text": "Good morning, how are you?" } } },
{ "id": "node2", "type": "llmChain", "label": "Translate → Chinese", "config": { "prompt": "Translate to Chinese, output only the translation: {{text}}", "temperature": 0 } },
{ "id": "node3", "type": "llmChain", "label": "Translate → Japanese", "config": { "prompt": "Translate to Japanese, output only the translation: {{text}}", "temperature": 0 } },
{ "id": "node4", "type": "output" }
],
"edges": [
{ "source": "node1", "target": "node2" },
{ "source": "node2", "target": "node3" },
{ "source": "node3", "target": "node4" }
]
}'# add a node and wire it between node1 and node3 in one call
curl -X POST https://api.simplynice.ai/api/ai/simple-flow/flows/$FLOW_ID/nodes -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
-d '{ "type": "llmChain", "label": "Summarise", "config": { "prompt": "Summarise in one line: {{text}}" },
"connectFrom": "node1", "connectTo": "node3" }'
# tweak one setting without touching the rest of the config
curl -X PATCH https://api.simplynice.ai/api/ai/simple-flow/flows/$FLOW_ID/nodes/node2 -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" -d '{ "configPatch": { "maxOutputTokens": 120 } }'Lay nodes out left → right: position.x in steps of ~320, y ≈ 200; branches at y − 140 / y + 140. Positions are cosmetic, but the user looks at the canvas — and the canvas live-syncs your edits within a few seconds.
Testing and running
Test one node with representative items before running the whole flow — it is cheaper and pinpoints prompt problems:
curl -X POST https://api.simplynice.ai/api/ai/simple-flow/flows/$FLOW_ID/nodes/node2/test -H "Authorization: Bearer $DJC_TOKEN" -H "Content-Type: application/json" \
-d '{ "inputItems": [ { "json": { "text": "Good morning, how are you?" } } ] }'
# → { "status": "success", "output": [ { "json": { "text": "早上好,你好吗?", "_llm": { "model": "qwen3.7-flash", "credits": 0.0003 } } } ] }Then run. Execution is asynchronous: you get an id immediately and poll until it finishes.
curl -X POST https://api.simplynice.ai/api/ai/simple-flow/flows/$FLOW_ID/execute -H "Authorization: Bearer $DJC_TOKEN"
# → 202 { "executionId": "3f9c…", "poll": "…/executions/3f9c…" }
curl https://api.simplynice.ai/api/ai/simple-flow/flows/$FLOW_ID/executions/$EXECUTION_ID -H "Authorization: Bearer $DJC_TOKEN"
# → { "status": "success", "log": [ { "nodeId": "node1", … }, { "nodeId": "node2", "output": [ … ] }, … ] }A node error stops the run with status: "error" and the failing node's error in its log entry. LLM nodes spend your credits on every test and run — use small maxOutputTokens and temperature: 0 for deterministic tasks.
WhatsApp-triggered flows
With a WhatsApp account connected in your workspace, a whatsappTrigger starts the flow on each matching inbound message (account, type and body filters; messages from yourself excluded by default). Downstream nodes read the message with {{body}}, {{from.name}}, {{from.phone}}, {{type}}, {{media.url}}. A sendWhatsapp node in reply mode answers in the same chat with no extra config.
{
"nodes": [
{ "id": "node1", "type": "whatsappTrigger", "config": { "includeFromMe": false,
"sampleData": { "body": "Hi, is the 3-bedroom unit still available?", "type": "text", "from": { "name": "Jane", "phone": "60123456789" } } } },
{ "id": "node2", "type": "llmChain", "label": "Draft reply",
"config": { "prompt": "You are a property concierge. Reply briefly and warmly to: {{body}}", "temperature": 0.4 } },
{ "id": "node3", "type": "sendWhatsapp", "config": { "mode": "reply", "message": "{{text}}" } }
],
"edges": [ { "source": "node1", "target": "node2" }, { "source": "node2", "target": "node3" } ]
}Give the trigger a realistic sampleData so tests and manual runs exercise the flow without waiting for a real message, and set active: true (via PATCH /flows/{flowId}) when it should go live. Reply mode errors on manual runs — there is no triggering message to answer.
Playbook
- Inspect first.
GET …/graph?includeOutput=1. Summarise nodes (id · label · type) before changing anything you weren't asked to change. - Prefer surgical edits.
PATCH …/nodes/{id}withconfigPatch. UsePUT …/graphonly to build from scratch — it replaces everything, including nodes made on the canvas. - Meaningful labels ("Translate → Chinese"), one-line descriptions.
- Test before running; then run and read
log[]. - Concurrency. The user's canvas syncs within ~4 s. Their saves are rejected if they'd overwrite yours; your writes are last-wins — re-
GETbefore aPUTif they may be editing. - Don't invent node types, delete the user's nodes to "simplify", or reference node ids you haven't seen in
GET …/graph.
Current limits: no "Set/Text" node, no generic webhook trigger, no loops or sub-flows; LLM nodes don't carry incoming fields through; merge fields read only the first item of a referenced node.
Every endpoint, with parameters and samples: Workflows reference.