Documentation menu

Guide

Workflows

Simple Flow is a small node-and-edge workflow builder. This guide covers the mental model — items, merge fields, branching — and how to build, test and run flows through the API.

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

TypeDoes
triggerManual start. Emits its config.sampleData on a run.
whatsappTriggerStarts the flow when a WhatsApp account connected in your workspace receives a matching message (only while the flow is active).
llmChainRuns a prompt through a model, once per input item. Output is { text } or the JSON fields you define.
httpRequestCalls a URL; a JSON response becomes the item.
condition (If)Routes each item to a true or false branch.
sendWhatsappSends one WhatsApp message per item — a reply to the triggering message by default.
outputTerminal; whatever reaches it is the flow's result.
actionPlaceholder 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:

NodeEmits
triggerIts sampleData: an object → 1 item; an array → one item per element.
llmChainReplaces the item with { text } (or your JSON fields) plus _llm: { model, inputTokens, outputTokens, credits }. Incoming fields are not carried through.
httpRequestOne item: the parsed JSON body, or { body, status } for other content types. Non-2xx is a node error unless ignoreResponseCode.
conditionEach item to one branch, with _if: { result, matched[] } added.
sendWhatsappThe 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:

SyntaxResolves 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.

Graph: amount > 100 → VIP reply, else basic reply
{
  "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:

Create with a graph
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" }
    ]
  }'
Surgical edits
# 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:

Test a node
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.

Run and poll
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.

Auto-reply: message → LLM draft → send
{
  "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

  1. Inspect first. GET …/graph?includeOutput=1. Summarise nodes (id · label · type) before changing anything you weren't asked to change.
  2. Prefer surgical edits. PATCH …/nodes/{id} with configPatch. Use PUT …/graph only to build from scratch — it replaces everything, including nodes made on the canvas.
  3. Meaningful labels ("Translate → Chinese"), one-line descriptions.
  4. Test before running; then run and read log[].
  5. Concurrency. The user's canvas syncs within ~4 s. Their saves are rejected if they'd overwrite yours; your writes are last-wins — re-GET before a PUT if they may be editing.
  6. 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.