Workflow Builder
Chain intents, agents, and tool calls into a visual, testable, deployable pipeline.
The Workflow Builder lets you chain multiple intents, agent runs, tool calls and human approvals into a single automation — built visually in the dashboard, testable before you deploy, and started by an API call, a signed webhook, a schedule, or an event elsewhere in your workspace.
Overview: is it ready, and how is it doing
A workflow opens on its Overview tab. It shows what the workflow is for, its status and version, how often its runs succeed, how many are waiting for someone, and when it last ran. Below that:
- Readiness lists what to fix before the workflow is ready, each with a button that takes you there: every step set up, every path ending at an End step, every
{{steps.…}}reference naming a real step, the workflow deployed, and at least one run completed. - Recent runs shows what each request was, where it got to, and links to its full trace in Logs.
Deploy makes the same kind of check on every reference: a name no step has, a step that cannot have run yet, or a field the step never returns are refused with a message that names the step and the reference.
Arranging and testing in the Builder
- Tidy up (top right of the canvas) arranges the steps neatly: the trigger on top, the main path straight down, branches side by side. A workflow drafted by Copilot is arranged this way automatically. Every step carries a number in the order it reads, so a step can be talked about ("step 7").
- Test run can start from a request that has run before: choose it under Start from a previous run and its input fills the box, so you don't retype a long input.
Connect: how to call a workflow
Open a workflow and choose the Connect tab. It shows whether the workflow is live, and, once it is, how to start it from your own system.
- API call. The tab builds a request for this workflow in curl, Node.js or Python: its address, your API key in an
Authorization: Bearerheader, anIdempotency-Key, and a body that already lists the fields the workflow's steps read from the trigger.user_idis required ininput, because it says who the run is for. A call to a file-reading workflow includes anattachmentsentry. - Webhook. For a workflow with a webhook trigger, the tab shows the address and how to sign the request: HMAC-SHA256 over
<timestamp>.<body>, sent asX-Liya-Timestamp,X-Liya-Signatureand a uniqueX-Liya-Event-Id. The signing secret is shown once, when you deploy; create a new one in Settings if you no longer have it. - What you get back. The call waits until the workflow finishes or pauses. The status says which:
completed,awaiting_approval,waiting,needs_input,failedorrejected. - Retries. An
Idempotency-Keystops a repeated call from running twice, and a workflow can limit how many of its runs execute at once.
A workflow that is not deployed cannot be called from outside. The Connect tab says so and points you to the Builder; Test run works from the Builder before that.
API reference
The dashboard (session-JWT, admin-only) owns the visual builder's convenience endpoints — the copilot and test-run:
GET /dashboard/workflows # list workflows
POST /dashboard/workflows # create a workflow
POST /dashboard/workflows/copilot # generate a draft from a natural-language description
GET /dashboard/workflows/:id # get a workflow + its steps
PATCH /dashboard/workflows/:id # update steps/config
DELETE /dashboard/workflows/:id # delete
PATCH /dashboard/workflows/:id/toggle # enable/disable (already-deployed workflows only)
POST /dashboard/workflows/:id/deploy # deploy to production
POST /dashboard/workflows/:id/test-run # run once without deployingEverything except the copilot and test-run is also available via API key at /v1/workflows — see the Workflows API reference for the full request/response shapes, including webhook-secret lifecycle and both SDKs.
Generate a draft with the copilot:
POST https://api.liyaengine.ai/dashboard/workflows/copilot
Authorization: Bearer <jwt>
Content-Type: application/json
{
"prompt": "When a support ticket comes in, check the knowledge base for a matching policy, draft a reply, and flag it for review if confidence is below 0.8."
}The copilot returns a draft workflow (steps, connections, suggested config) you can edit in the visual builder before saving.
Test run before deploying:
POST https://api.liyaengine.ai/dashboard/workflows/wf_01HZ.../test-run
Authorization: Bearer <jwt>
Content-Type: application/json
{ "input": { "ticket_id": "tkt_123" } }Returns the full step-by-step execution graph — the same view the dashboard's workflow builder renders — without affecting production traffic.
Step types
A workflow step can be an intent call, an agent run, a tool call (action), a conditional branch, a switch, a delay, a loop, a set-fields step, a state step that keeps values between runs, a notification, a wait for an outside callback, a call to another workflow, parallel branches, or a human approval. Steps pass structured output forward via input mapping, so a later step can reference an earlier step's result directly by its name: {{steps.extract_request.output.service}} reads the service field from the step named Extract request. See Referring to earlier steps.
An action step has its own Input mapping: list an action parameter on the left and its value on the right ({{trigger.request_ref}} or {{<step id>.output.letter}}). Mapped values take priority; a parameter you don't map is read from the trigger input field with the same name. Values are text, and are converted to a number or true/false when the action declares that parameter type.
Referring to earlier steps
A step reads an earlier step's result with {{steps.<name>.output.<field>}}, where <name> is the step's name in lower case with spaces and punctuation turned into underscores: Extract request becomes extract_request, Meets criteria? becomes meets_criteria. The step's Details tab shows the exact reference. {{steps.<name>.output}} passes the whole result as JSON text.
The same references work in a condition's or switch's Field (without braces: steps.review.output.recommendation) and in a loop's list. Every mapping box has an Insert menu that lists the trigger's input and each earlier step's fields, so you pick instead of type. Intent steps list the fields their output schema declares.
- Rename a step and its references change, so rename first, then reference.
- Two steps with the same name are told apart by order: the first is
review, the secondreview_2. - The older form with the step's internal id (
{{<step id>.output.field}}) still works.
Set fields
A Set fields step builds new values from earlier results, so you don't need an extra intent just to reshape data. Add one field per value: a name (letters, digits and underscores), a value that can mix text and references, and a type: Text, Number, True / false, or List or object (JSON). Read the result later as {{steps.<this step>.output.<field name>}}.
{ "step_type": "transform", "config": { "fields": [
{ "name": "label", "value": "Auth for {{steps.extract_request.output.member_ref}}", "type": "text" },
{ "name": "codes", "value": "{{steps.extract_request.output.procedure_codes}}", "type": "json" }
] } }A reference that can't be found, or a value that can't be converted to its type, fails the step with a message naming the field, instead of passing a gap along.
Retries and time limits
An Action, Intent or Agent step can be given a retry policy and a time limit under Retries and time limit in its settings.
| Setting | Meaning |
|---|---|
Tries (retry.max_attempts) | How many times to run the step before treating it as failed, 1 to 5. Default 1. |
Wait between tries (retry.backoff_seconds, retry.backoff) | 1 to 30 seconds, either the same each time (fixed) or doubling (exponential, the default). |
Time limit (timeout_seconds) | Stop waiting after this many seconds, 1 to 300. |
{ "step_type": "ai_intent", "config": { "intent_id": "…", "retry": { "max_attempts": 3, "backoff_seconds": 2, "backoff": "exponential" }, "timeout_seconds": 60 } }Errors that a second try cannot fix are not retried: bad input, a missing or archived intent, a spent quota. The run log shows how many tries a step took, and a failure reads … (after 3 attempts).
A time limit stops the workflow waiting; the call it started may still finish on the other side. So a timed-out Action is never retried, and you should retry an action only when repeating it is safe.
Versions and rollback
Every time you Deploy or Redeploy a workflow, and every time you restore one, its steps, connections and settings are saved as a numbered version. Open the Versions tab to see them, newest first, with the one that is live marked Current. View changes shows what changed since the version before it: steps added or removed, a step whose settings or connections changed, a switch case or loop exit that now goes somewhere else, and a renamed workflow or changed trigger.
Restore puts an older version's steps and connections back. If the workflow is live, the restored graph is live at once. The restore is itself saved as a new version (Restored from version 2), so history is never rewritten and you can restore the one you left. A run that is already waiting at an approval or delay continues on the steps it saved. If the restored version has a different trigger type, deploy again to get the new webhook address and secret. A version that points at an action or intent that has since been deleted can't be restored until you fix that reference.
Editing a live workflow
While a workflow is live, Save keeps your edits as unpublished changes. Live runs, the API and webhooks keep using the published steps, and the editor shows a banner with Discard changes. Test run uses your unpublished changes, so you can check them first. Publish changes (the Deploy button) validates them and makes them live as a new version; if the changes aren't valid, nothing goes live and the message names each problem. Restoring an older version discards any unpublished changes. A version is recorded each time you publish, and each run records the version that was live when it started (shown on the run as Version).
Over the API: GET /v1/workflows/{workflow}/versions, GET /v1/workflows/{workflow}/versions/{n} (with changes), and POST /v1/workflows/{workflow}/versions/{n}/restore.
Failed and waiting runs
Retry from the failed step. When a step fails with nowhere to go, the run keeps the results so far. Open the run in Logs and choose Retry from the failed step (or POST /v1/workflows/{workflow}/runs/{runId}/retry). The same run continues at that step, with the earlier results intact, so work already done isn't repeated. If the step fails again the run stays failed and can be retried again. A run that failed before it saved a step, or any run that isn't failed, can't be retried.
Waiting for a callback. A run waiting at a Wait for callback step shows waiting with resume_at set to its time limit. It continues when the callback arrives, or follows the step's timed-out connection when the time is up (checked about once a minute).
Cancel a run. A run that is awaiting_approval, waiting or needs_input can be cancelled with Cancel run (or POST /v1/workflows/{workflow}/runs/{runId}/cancel). Pending approvals for it are closed and the run ends as cancelled. A run that is executing right now cannot be interrupted.
Failure alerts. A failed run raises a notification for the workspace, at most one per workflow every 15 minutes, linking to the run. The workflow.failed event also fires, so a workflow subscribed to it can send the failure to Slack, Teams or email through an action. A run left in running for over an hour, for example because the server restarted, is stopped and marked failed with the same alert.
Switch, delay and loop
These three steps control the path a run takes. Each is configured in the builder and saved in the step's config.
Switch
A switch sends the run down one of several paths, chosen by the value of a field. Set Field to a dot-path (trigger.service_type, <step id>.output.recommendation, or loop.item inside a loop) and add one case per value. The run follows the first case whose value equals the field, compared as text, the same way a condition's equals works. Each case has its own connection on the step.
The red Otherwise connection runs when no case matches. With no Otherwise connection, a non-matching run fails with no case matched <field>. A case left unconnected ends that path.
{ "step_type": "switch", "config": { "field": "trigger.kind", "cases": [ { "id": "c1", "value": "refund", "next_step_id": "<step id>" } ] } }Delay
A delay pauses the run for a set time, then continues with the next step. Set an amount and a unit (minutes, hours or days), from 1 minute to 30 days. While it waits, the run has status waiting and the response carries resume_at. Nothing is held open: the wait is saved with the run, so it survives restarts and deploys. A background sweep continues each run within about a minute of its resume_at.
{ "step_type": "delay", "config": { "amount": 2, "unit": "hours" } }Loop
A loop runs the steps connected to Each item once for every item of a list, then continues from When done. Set List to walk to a reference that holds a list, such as {{trigger.procedure_codes}} or {{<step id>.output.items}}. The list can be a JSON array, one item per line, or comma-separated. Maximum items defaults to 25 and can be up to 100; a longer list fails the run instead of skipping items.
Inside the loop body, read the current item with {{loop.item}} (or {{loop.item.field}} when items are objects), {{loop.number}} (from 1), {{loop.index}} (from 0) and {{loop.count}}. Each run of a body step appears in the run log labelled with its item, for example Review [item 2 of 5]. When the loop finishes, {{<loop step id>.output.results}} holds each item's last result.
- End the Each item path with an End step, or connect its last step back to the loop. An unconnected step would otherwise continue to whichever step comes next in order, so deploying is refused until the path ends.
- Loops cannot be nested.
- A step inside the loop that fails stops the workflow, unless that step has a Failure connection.
- An approval or delay inside a loop pauses the run and keeps its place in the list.
- An empty list skips the body and continues from When done.
Deploying a workflow with an incomplete switch (no field or cases, duplicate values), delay (no usable wait) or loop (no list, no Each item step, nested loop) is refused with a message naming each problem.
Notify, callbacks, other workflows and parallel branches
State
A run's own data (the trigger input and each step's result) disappears when the run ends. A State step keeps a value under a key for the whole workflow, so a later run can read it. Choose what to do:
- Read a value returns
{{steps.<name>.output.found}}(true or false) and{{steps.<name>.output.value}}. Give a value if nothing is kept (with a type) to get a default instead of an empty result, or tick Fail this step if nothing is kept to follow the Failure connection. Follow a read with a Condition onfound. - Keep a value writes
Value(as text, a number, true/false or JSON) under the key and returnswritten,valueandprevious. Tick Only if nothing is kept under this key yet for a once-only marker:writtenis false when a value was already there, which is a simple "have we already done this?" check. - Add to a number adds an amount (negative to subtract; a missing value starts at 0) and returns the new total. It is a single atomic step, so runs going at the same moment never lose a count. It fails if the value kept under the key isn't a number.
- Delete a value removes the key and returns
deleted.
The key can include references, so member:{{trigger.member_id}} keeps one value per member. Set Keep for (days) (1 to 365) on anything about a person so it expires on its own; expired values read as missing and are removed within a minute. Values belong to one workflow, up to 16 KB each and 1,000 keys. Editor Test runs use a separate copy, so trying a workflow never changes what the live one sees. The workflow's Settings tab lists what is kept, with a Delete for each; over the API, GET /v1/workflows/{workflow}/state and DELETE /v1/workflows/{workflow}/state/{key} do the same. State is part of run storage: keep values small, avoid storing identifying details you don't need, and use an expiry.
Notify
Sends a message from inside a workflow. Set a Title, an optional Message and a Severity (info, warning or error); references such as {{trigger.request_ref}} are filled in, and a reference that doesn't resolve fails the step instead of sending a half-written message. Under Where to send it, choose any mix of:
- Notification bell: the whole workspace, or one person if you give a user id (or a reference that holds one). The notification links to the run.
- Email: up to 10 addresses, comma-separated, with references allowed (
{{trigger.owner_email}}). Each person gets their own message, sent from your workspace's email sender. Email isn't a safe place for health or personal details, so send a link to the run instead of the details. - Slack and Teams: tick a saved destination, or choose + Add destination, give it a name, pick Slack or Teams and paste the incoming-webhook address. The address is checked against the real Slack and Teams hosts, stored encrypted and never shown again (the list shows only a hint), so it never ends up in a workflow's version history. Delete a destination with the × beside it; that removes it for every workflow that used it. Up to 25 destinations per workspace and 5 per step.
The step succeeds if the message reached at least one place, and its result lists any that failed (email.failed, destinations[].error); if it reached none, the step fails. Test runs send for real, marked [Test], so use a test channel while you build.
Wait for callback
Pauses the run until an outside system reports back, for example a vendor, a scan service or a person working in another tool. Set Give up after (a time limit up to 30 days; it is required so a run can never wait forever). The run shows as Waiting. Put {{run.callback_url}} in an earlier action's mapping so the other system knows where to call: it sends a POST with a JSON body to that address, and the body becomes this step's result ({{steps.<name>.output.<field>}}). A body containing "success": false, or the time limit passing, follows the Timed out connection (with none, the run fails). The address contains a signature only LiyaEngine can make, works once, and stops working when the run is cancelled or has moved on (409 CALLBACK_NOT_WAITING). Nothing is held open while waiting, so a callback wait survives restarts and deploys.
Run workflow
Runs another live workflow from this one, waits for it to finish, and continues with its result. Choose the workflow and map its Input (what it receives as its trigger input). The called workflow's last result is available as {{steps.<name>.output.output}}, and its run is linked to this one in its history. If it fails, this step follows its Failure connection. Limits: the called workflow can't contain an approval, delay or wait for callback (it would have to pause), can't call this workflow back (including through a chain), and workflows can be nested three deep.
Parallel
Runs two to five branches at the same time, then continues when all are done. Add branches, drag from each branch's dot to its first step, and end every branch with an End step; connect When all done to the step that follows. Branches can't see each other's results while running, and can't contain approvals, delays, callbacks, loops or another parallel step. After the join, read any branch step's result by its name, or a branch's last result at {{steps.<name>.output.branches.<branch>.output}}. If a branch fails you choose between failing the step (it follows its Failure connection) or carrying on with the others. Every branch always runs to its end; when you retry a failed run from the parallel step, branch steps that already succeeded aren't repeated. Saving is refused when branches share a step, a branch isn't ended, or one runs into the join.
Retention and minimal run storage
Open Settings → Data retention (admins only) to choose how long LiyaEngine keeps what your workflows and agents produce. A nightly job deletes anything older: finished workflow runs with their steps and approval requests (default 90 days), workflow events (30 days), finished agent runs and idle sessions (90 days). Conversation sessions (30 days) and messages (90 days) are set the same way and apply to data stored from then on. Each window is 1 to 3,650 days; shorter is safer for health or personal data.
Workflow run storage is Full (the default) or Minimal. With Minimal, a run keeps when it happened, which steps ran, whether they worked and how long they took, but its input and each step's result are stored as field names and types only, never the values. A caller still receives the real result of a run, and a run waiting at an approval, delay or callback keeps the values it needs to continue until it finishes (a failed run keeps them so it can be retried, until its retention window passes). Approval requests keep what the approver is deciding on, for the retention window. In Minimal storage, workflow events are kept for 7 days unless you set a window. Minimal storage applies from the next run on; the Saved values a State step keeps are not affected, so give anything about a person an expiry.
Concurrency and repeated calls
Concurrency limit. In a workflow's Settings tab, set the most runs that may execute at the same moment (1 to 100, or none). Runs waiting at an approval, delay or callback don't count. A call over the limit is refused with 429 CONCURRENCY_LIMIT and a Retry-After header, and a scheduled or event run over the limit is skipped. Test runs are never limited.
Idempotency keys. Send an Idempotency-Key header on POST /v1/workflows/{workflow}/run (or /run/stream). A repeated call with the same key returns the first run's result, marked idempotent_replay: true, instead of starting another, so a client that retries after a timeout never runs the workflow twice. Keys are per workflow, 1 to 128 characters (letters, digits, . _ : -). If the first call is still running, the repeat returns status: "running" with the same run_id.
Triggers
Every workflow has one trigger step. Set its type in the builder, or config.trigger_subtype over the API.
| Trigger | Starts a run when | Run input ({{trigger.*}}) |
|---|---|---|
manual | You call POST /v1/workflows/:id/run or test from the builder | The request's input |
webhook | An external system posts to the signed webhook URL minted on deploy | The posted JSON body |
schedule | A cron schedule fires | config.input, plus scheduled_for and triggered_at |
event | Something happens in your workspace | The event's fields, plus event_type and event_id |
Schedule — { "trigger_subtype": "schedule", "cron": "0 7 * * 1-5", "timezone": "America/Chicago", "input": { "region": "US" } }. Standard 5-field cron, any IANA timezone (daylight saving handled), at most every 5 minutes. The schedule starts when the workflow is deployed and stops when it's disabled. If a previous scheduled run is still executing, that slot is skipped; slots missed during downtime aren't replayed.
Event — { "trigger_subtype": "event", "event_type": "document.ingested", "filter": { "collection_id": "col_123" } }. Leave filter empty to run on every matching event.
| Event | Fires when | Filters |
|---|---|---|
document.ingested | A knowledge document finishes ingesting (upload, push, URL or file job) | collection_id, domain_key |
workflow.completed | Another workflow's run completes | workflow_id |
workflow.failed | Another workflow's run fails or is rejected | workflow_id |
workflow.approval_requested | A workflow pauses for approval — use it to post the request to Slack, Teams or email through an action | workflow_id |
workflow.approval_decided | An approval is approved, rejected or times out | workflow_id |
eval.run_completed | An evaluation suite run finishes | suite_id |
Events are delivered at least once from a durable outbox, so key any side effect on {{trigger.event_id}}. A workflow never receives its own workflow.* events, and chains of event-triggered workflows stop after 3 hops. GET /v1/workflows/trigger-catalog returns this list.
Approval steps
An approval step pauses the run until a person decides. The run's status becomes awaiting_approval and the response carries an approval_id. Approving continues down the step's Approved connection; rejecting follows its Rejected connection, or ends the run with status rejected if there isn't one. Later steps can read {{steps.<name>.output.decision}}, .note and .decided_by.
Reminders and escalation. Set Remind after (hours) to notify the approvers if nobody has decided (the named users, or the whole workspace for a role-based request). Set Escalate after (hours) and a role to reassign the request to that role or higher when it is still undecided, before any automatic outcome; the inbox then marks it Escalated. The order is remind, then escalate, then the time limit, so each must be earlier than the next. Deploying is refused when they are out of order or an escalation has no role.
Configure on the step: a title and details for the approver (both accept {{variables}}), who may decide (a minimum role, or named people — admins can always override), and an optional time limit with an automatic approve or reject when it runs out.
Decide from the Approvals inbox in the dashboard, or from your own tools:
GET /v1/workflows/approvals?status=pending
GET /v1/workflows/approvals/:approvalId
POST /v1/workflows/approvals/:approvalId/decision
{ "decision": "approve", "note": "Within policy", "decided_by": "[email protected]" }The decision response includes the resumed run's status. Every decision is written to the audit log with who made it; API decisions record decided_by alongside the API key.