Automation Workflows
This guide walks through building real flows with the Automation engine: a flow is a graph of typed nodes, triggered by an event, that branches on conditions, enriches with lookups, and sends through your providers. Each example below is a complete, publishable flow.
Anatomy of a flow
A flow definition has a nodes array and a separate edges array — nodes hold the work, edges wire them together:
- Nodes carry
id,type, and aconfigobject. - Edges carry
source,target, and atype—success(ordefault),failure, orbranch. Branch edges also carry akeythat the routing node returns. saveAsstores a node's output under a variable name.{{ }}expressions read upstream data inside config values:{{ input.payload.* }}— the triggering event{{ vars.* }}— variables prior nodes saved{{ nodes.<id>.output.* }}— a specific node's result
Drafts are editable; publications are immutable. Always simulate a draft (a full dry-run with no side effects) before publishing.
Example 1 — Welcome email on signup
When a customer is created, enrich the record and send a welcome email through your SendGrid account.
{
"name": "Welcome email on signup",
"version": "1",
"enabled": true,
"triggerType": "event",
"nodes": [
{ "id": "start", "type": "start" },
{
"id": "lookup", "type": "lookup_customer",
"config": { "customerId": "{{ input.payload.customerId }}", "saveAs": "customer" }
},
{
"id": "tpl", "type": "render_template",
"config": { "template": "Welcome, {{ vars.customer.firstName }}!", "saveAs": "body" }
},
{
"id": "email", "type": "send_email",
"config": {
"provider": "sendgrid",
"apiKeySecretRef": "vault:sg_prod",
"to": "{{ vars.customer.email }}",
"subject": "Welcome to Acme",
"body": "{{ vars.body }}",
"saveAs": "emailResult"
}
},
{ "id": "stop", "type": "stop" }
],
"edges": [
{ "id": "e1", "source": "start", "target": "lookup", "type": "success" },
{ "id": "e2", "source": "lookup", "target": "tpl", "type": "success" },
{ "id": "e3", "source": "lookup", "target": "stop", "type": "failure" },
{ "id": "e4", "source": "tpl", "target": "email", "type": "success" },
{ "id": "e5", "source": "email", "target": "stop", "type": "success" }
]
}Example 2 — Branch on order value
When an order is paid, take a different path for high-value orders. The if node evaluates its first case and returns a branch key; the runner follows the branch edge whose key matches.
default branch — the flow never ends silently because no branch matched. Do not leave the default empty.{
"nodes": [
{
"id": "branch", "type": "if",
"config": {
"cases": [
{ "field": "input.payload.order.total", "op": "gt", "value": 50000, "key": "true" }
],
"defaultEdgeKey": "default"
}
}
],
"edges": [
{ "id": "b1", "source": "branch", "target": "smsVip", "type": "branch", "key": "true" },
{ "id": "b2", "source": "branch", "target": "emailReceipt", "type": "branch", "key": "default" }
]
}In an if / switch case, field is a plain path into the run context (input.payload.order.total), not a {{ }} expression — the engine reads the value at that path. Operators: eq, neq, gt, gte, lt, lte, in.
For more than two branches, use switch (pick a branch edge by value) or rule (a nested all / any / none condition tree that routes to success or failure).
Example 3 — Enrich, validate, then act
Before sending, look up the payment, validate the resolved object, and only continue if the shape is correct.
{
"nodes": [
{
"id": "pay", "type": "lookup_payment",
"config": { "paymentId": "{{ input.payload.paymentId }}", "saveAs": "payment" }
},
{
"id": "check", "type": "validate_schema",
"config": {
"target": "{{ vars.payment }}",
"rules": [
{ "field": "status", "required": true, "enum": ["captured", "refunded"] },
{ "field": "amount", "required": true, "type": "number", "min": 1 }
]
}
}
],
"edges": [
{ "id": "e1", "source": "pay", "target": "check", "type": "success" },
{ "id": "e2", "source": "pay", "target": "stop", "type": "failure" },
{ "id": "e3", "source": "check", "target": "act", "type": "success" },
{ "id": "e4", "source": "check", "target": "stop", "type": "failure" }
]
}Connecting your providers
The send_email, send_sms and send_invoice nodes deliver through your provider with your key. Store the credential in the vault first, then reference it by secretRef — never inline the raw key.
{
"type": "send_email",
"config": {
"provider": "sendgrid",
"apiKeySecretRef": "vault:sg_prod",
"to": "{{ vars.customer.email }}",
"templateId": "welcome_v2",
"variables": { "name": "{{ vars.customer.firstName }}" }
}
}Need a provider that isn't built in? Use generic_http_email or generic_http_sms and define the full request (URL, method, headers, body) yourself.
Build, simulate, publish
Create a draft
curl https://api-gateway.lunixi.com/api/v1/automation/flows/draft \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-H "Content-Type: application/json" \
-d @welcome-flow.jsonValidate the graph
curl https://api-gateway.lunixi.com/api/v1/automation/flows/validate \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-d @welcome-flow.jsonSimulate (full dry-run, no side effects)
Sends, webhooks and HTTP calls are skipped and replaced with synthetic results; downstream nodes still resolve so you can inspect the whole path.
curl https://api-gateway.lunixi.com/api/v1/automation/flows/{id}/simulate \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-d '{ "input": { "payload": { "customerId": "cus_123" } } }'Publish
Publishing compiles the draft into an immutable version. A checksum gate requires a fresh validate + simulate after any change.
curl https://api-gateway.lunixi.com/api/v1/automation/flows/{id}/publish \
-H "Authorization: Bearer at_live_5f3e2b1a..." -d '{}'Driving the flow from your storefront
Register an integration to get a signed ingress URL and an HMAC secret, then post events from your app. WordPress / WooCommerce events are normalised to canonical commerce.* keys automatically.
TS=$(date +%s)
BODY='{"event":"order.paid","data":{"order":{"id":"1042","total":52000}}}'
SIG=$(printf '%s.%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$SIGNING_SECRET" -hex | sed 's/^.* //')
curl https://api-gateway.lunixi.com/api/v1/automation/integrations/wordpress/intg_123/events \
-H "X-Automation-Signature: sha256=$SIG" \
-H "X-Automation-Timestamp: $TS" \
-H "Content-Type: application/json" \
-d "$BODY"The response status mirrors the outcome (202 accepted, 200 duplicate, 401 bad signature, 409 replayed nonce, 429 rate-limited), so producer-side retries behave correctly.
Governing manual runs
For flows that move money or message customers, gate manual runs through an approval queue. Set the merchant default policy, or override it per flow:
curl -X PUT https://api-gateway.lunixi.com/api/v1/automation/merchant-policy \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-d '{ "defaultManualRunPolicy": "approval" }'With approval, a manual run enqueues a pending request instead of executing. A reviewer with the automation:approve scope decides:
curl https://api-gateway.lunixi.com/api/v1/automation/approvals/{id}/decide \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-d '{ "decision": "approve", "note": "Verified order total" }'Approving runs the flow against its published version. The full request payload is sealed and used as the only run source; reviewers see a redacted projection.
Observing runs
Pull per-flow run history and drill into a single run's node-by-node trace:
# Recent runs for a flow
curl "https://api-gateway.lunixi.com/api/v1/automation/flows/{id}/runs?limit=50" \
-H "Authorization: Bearer at_live_5f3e2b1a..."
# A single run's redacted trace
curl https://api-gateway.lunixi.com/api/v1/automation/flows/{id}/runs/{runId} \
-H "Authorization: Bearer at_live_5f3e2b1a..."You can also query the overview dashboard, the inbound trigger-event log, the outbound connector-delivery log, and the append-only audit trail. See Automation for the full set.