Guides
Campaigns and workflows
Chain calls, waits, retries and API requests into a workflow, then run it for one contact or a whole list.
A batch makes one call per contact. A workflow describes what happens around the call: wait, retry on busy, call your API with the result, branch on the outcome. A workflow campaign runs a published workflow for a list of contacts.
In the dashboard: Build → Workflows and Deploy → Campaigns. Both are marked beta there.
Note: Workflow endpoints use their own error body:
{"detail": {"code": "...", "message": "..."}}, sometimes with extra fields. Everything else in the API uses{"error", "message"}.
1. Create a workflow
curl -X POST https://api.kshyamai.space/workflows \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"name": "Loan follow-up"}'The response (201) has the workflow's id. Its draft starts out empty, at revision 0.
2. Save a draft
A definition has an entry_node_id, an on_no_match node, and nodes. Each node has an id, a type, a config, and cases that say where to go next.
curl -X PUT https://api.kshyamai.space/workflows/WORKFLOW_ID/draft \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{
"expected_revision": 0,
"definition": {
"entry_node_id": "n_start",
"on_no_match": "n_end_unhandled",
"nodes": [
{"id": "n_start", "type": "start", "config": {"trigger": {"kind": "manual"}},
"cases": [{"when": {"always": true}, "then": {"to": "n_call"}}]},
{"id": "n_call", "type": "agent",
"config": {"agent_id": "3c90c3cc-0d44-4b50-8822-8dd25736052a", "timeout_s": 3600,
"variables": {"name": {"value": {"var": "entry.name"}}}},
"cases": [
{"when": {"cmp": "==", "left": {"var": "call.status"}, "right": {"const": "completed"}},
"then": {"to": "n_end_reached"}},
{"when": {"cmp": "in", "left": {"var": "call.status"}, "right": {"const": ["busy", "no_answer"]}},
"then": {"to": "n_retry"}}
]},
{"id": "n_retry", "type": "retry",
"config": {"target_node_id": "n_call", "attempts": [{"delay": {"minutes": 2}}, {"delay": {"minutes": 2}}]},
"cases": [{"when": {"always": true}, "then": {"to": "n_end_unreachable"}}]},
{"id": "n_end_reached", "type": "end", "config": {"label": "reached", "outcome": "success"}},
{"id": "n_end_unreachable", "type": "end", "config": {"label": "unreachable", "outcome": "failure"}},
{"id": "n_end_unhandled", "type": "end", "config": {"label": "unhandled", "outcome": "failure"}}
]
}
}'{"revision": 1}expected_revision must match the draft's current revision (read it with GET /workflows/{workflow_id}/draft). A stale value is 409 revision_conflict with current_revision. A definition over 512 KB is 413 definition_too_large.
Node types
| Type | What it does |
|---|---|
start | Entry point. Declares the contact fields the workflow accepts |
agent | Places a call with one of your agents and waits for it to finish. Branch on call.status |
extraction | Exposes fields extracted from the preceding call as extraction.* |
api | Calls an HTTP endpoint, maps values from the response, and branches |
time | Waits for a delay or until a timestamp |
retry | Re-runs an earlier node on a schedule, up to the listed attempts |
aisensy_whatsapp | Sends a WhatsApp template through the operator's configured message provider. Single exit |
end | Ends the run with a label and an outcome (success, failure, neutral) |
GET /workflow-node-types returns every type with its parameters, defaults and limits. Call statuses a workflow sees are completed, busy, no_answer, failed, voicemail, cancelled, rejected, timeout, error.
Conditions
A case's when is one of: {"always": true}, a comparison {"cmp", "left", "right"} with ==, !=, >, >=, <, <=, in, not_in or contains, or a combination with all, any or not. Operands are {"var": "call.status"} or {"const": ...}. Variable namespaces: entry, call, extraction, node, response, state, whatsapp.
3. Validate and publish
POST /workflows/{workflow_id}/validate returns {"issues": [...]}, each with a severity. POST /workflows/{workflow_id}/publish freezes the draft as a numbered version:
{"version": 1, "published_at": "2026-10-13T09:50:00Z", "warnings": []}Publishing fails with 422 invalid_definition (the issues are included) if the draft has errors, and 409 already_published if nothing changed since the last version. Runs and campaigns always use a published version, never the draft.
4. Run it for one contact
curl -X POST https://api.kshyamai.space/workflows/WORKFLOW_ID/run \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"reference_id": "lead_10442", "mobile_number": "+919876500123", "name": "Asha"}'{"execution_id": "exec:CAMPAIGN_ID:lead_10442", "campaign_id": "CAMPAIGN_ID", "workflow_version": 1, "status": "pending"}The built-in contact fields are reference_id, mobile_number, name and email; anything else must be declared on the start node, or the run is refused with 422 unknown_field. A reference_id runs once per version: a repeat is 409 duplicate_run. If you send none, one is derived from the row.
Follow it with GET /workflow-executions/{execution_id} (URL-encode the id; it contains colons). It reports status, current_node_id, outcome, termination_reason, each node attempt and recent events. Final statuses are completed, failed, cancelled and aborted. POST /workflow-executions/{execution_id}:cancel stops one.
Agent nodes place ordinary calls, so concurrency, compliance checks, billing and post-call data all apply.
5. Run it for a list: campaigns
curl -X POST https://api.kshyamai.space/workflow-campaigns \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
-H "Content-Type: application/json" \
-d '{"workflow_id": "WORKFLOW_ID", "name": "October follow-ups"}'The campaign pins the latest published version (or the version you name) and starts as draft. Optional: scheduled_at, and binding with agent_overrides and number_overrides keyed by agent node id.
Upload entries
Send CSV (Content-Type: text/csv) or a JSON array of objects. GET /workflow-campaigns/{campaign_id}/entries/template gives the CSV header for the pinned version.
curl -X POST https://api.kshyamai.space/workflow-campaigns/CAMPAIGN_ID/entries \
-H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
-H "Content-Type: text/csv" \
--data-binary @entries.csv{"accepted": 2, "corrected": 0, "failed": 1, "entries_count": 3,
"failures": [{"row": 4, "reference_id": "lead_3", "code": "invalid_phone",
"message": "mobile_number: expected an E.164 phone number, e.g. +919876543210"}],
"warnings": []}Rows that fail validation are kept as failed_validation so you can list them and upload a corrected row with the same reference_id. One upload can hold up to 150,000 rows and 32 MB. Adding entries works in the draft, scheduled and running states.
Control it
| Call | Effect | Allowed when |
|---|---|---|
POST /workflow-campaigns/{campaign_id}:start | Becomes scheduled, then running | draft that still has pending entries |
POST /workflow-campaigns/{campaign_id}:pause | paused | scheduled or running |
POST /workflow-campaigns/{campaign_id}:resume | Continues | paused |
POST /workflow-campaigns/{campaign_id}:abort | aborted; open runs end aborted | Not completed or aborted |
Each answers {"status": "..."}. A call in the wrong state is 409 invalid_state with campaign_status. A campaign pauses itself with paused_reason: "balance-low" when the wallet is empty, and resumes after a top-up.
Report
GET /workflow-campaigns/{campaign_id}/report:
{"executions": {"completed": 2},
"termination_reasons": {"reached": 1, "unreachable": 1},
"funnel": [{"node_id": "n_call", "status": "completed", "count": 2}]}The campaign itself also carries success_count, failure_count, neutral_count and terminal_count.
Webhooks
Set one per workflow with PATCH /workflows/{workflow_id} and {"settings": {"webhook": {"url": "https://...", "headers": {...}}}}. The URL must be https on a public host. POST /workflows/{workflow_id}/webhook:test sends a sample, and POST /workflow-executions/{execution_id}/webhook:resend re-queues a finished run's payload.
Limitations
- The only start trigger is
manual: runs begin through the API or a campaign, not from an external event. - Workflow webhook deliveries have no dead-letter view. Call webhooks do; see Webhooks.
- The WhatsApp node depends on a message provider the operator must configure. The shipped adapters have only been tested against local fakes.
- A paused campaign does not interrupt calls that are already in progress.