Graine AI

Workflows API

Build, check, publish and run outreach workflows with an API key — the endpoints, the scopes they need, and a run from start to finish.

Everything you do with workflows in the dashboard you can do with an API key, except four things done in the dashboard: turning on the trigger URL and Start when a call ends (by someone whose starts need no approval), rotating keys, and adding messaging connections (workspace admins). The full request and response shapes are in the API reference under Workflows.

EndpointDoesScope
GET /v2/workflows, POST /v2/workflowsList, create (from a definition, or a sample: {"sample", "agent_id"})agents:read, agents:write
GET /v2/workflow-samplesThe sample workflows, with their definitionsagents:read
GET /v2/workflows/{workflow_id}The draft, its versions and its URLsagents:read
PUT /v2/workflows/{workflow_id}/draftSave the draft (send the revision you read)agents:write
POST /v2/workflows/{workflow_id}/validateCheck a definition; saves nothingagents:write
POST /v2/workflows/{workflow_id}/publishThe draft as the next versionagents:write
GET /v2/workflows/{workflow_id}/versions/{number}A published versionagents:read
POST /v2/workflows/{workflow_id}/runs, GET …/runsStart a run for one contact, list runscalls:write, calls:read
GET /v2/workflow-runs/{run_id}, POST …/cancelA run with its timeline, end a runcalls:read, calls:write
POST /v2/workflow-campaigns, GET …Create a list (pins a version), list thembatches:write, calls:read
POST /v2/workflow-campaigns/{workflow_campaign_id}/entriesAdd contacts, before it startsbatches:write
POST /v2/workflow-campaigns/{workflow_campaign_id}/start, /pause, /resume, /cancelRun itbatches:write

A key acts with the role of the person who created it. A key a Member created cannot do what that Member could not do in the dashboard, and when your workspace asks for approval before a list starts, its start answers 202: it starts the moment an admin approves. A key limited to some agents only sees workflows whose agents are all among them.

From draft to run

Create the workflow with its definition:

curl -X POST https://api.graine.ai/v2/workflows \
  -H "Authorization: Bearer $GRAINE_API_KEY" -H "Content-Type: application/json" \
  -d '{"name": "New leads", "definition": {
        "entry_node_id": "start", "on_no_match": "unreached",
        "nodes": [
          {"id": "start", "type": "start", "config": {"fields": [{"name": "plan", "type": "string"}]},
           "cases": [{"when": {"always": true}, "then": {"to": "call"}}]},
          {"id": "call", "type": "agent", "config": {"agent_id": "agt_0123"},
           "cases": [{"when": {"cmp": "==", "left": {"var": "call.status"}, "right": {"const": "completed"}},
                      "then": {"to": "reached"}},
                     {"when": {"cmp": "in", "left": {"var": "call.status"}, "right": {"const": ["busy", "no_answer"]}},
                      "then": {"to": "again"}}]},
          {"id": "again", "type": "retry",
           "config": {"target_node_id": "call", "attempts": [{"delay": {"minutes": 30}}, {"delay": {"hours": 4}}]},
           "cases": [{"when": {"always": true}, "then": {"to": "unreached"}}]},
          {"id": "reached", "type": "end", "config": {"label": "reached", "outcome": "success"}},
          {"id": "unreached", "type": "end", "config": {"label": "unreached", "outcome": "failure"}}
        ]}}'

Check it, then publish:

curl -X POST https://api.graine.ai/v2/workflows/owf_8a1d4e7c2b90f1a2b3c4d5e6/validate -H "Authorization: Bearer $GRAINE_API_KEY"
curl -X POST https://api.graine.ai/v2/workflows/owf_8a1d4e7c2b90f1a2b3c4d5e6/publish  -H "Authorization: Bearer $GRAINE_API_KEY"
{ "valid": true, "problems": [], "warnings": [] }

Start a run for one contact, then follow it:

curl -X POST https://api.graine.ai/v2/workflows/owf_8a1d4e7c2b90f1a2b3c4d5e6/runs \
  -H "Authorization: Bearer $GRAINE_API_KEY" -H "Content-Type: application/json" \
  -d '{"contact": {"mobile_number": "+919876543210", "name": "Asha", "plan": "gold"}}'
 
curl https://api.graine.ai/v2/workflow-runs/oex_2c9e8f1a7b3d4c5e6f7a8b9c -H "Authorization: Bearer $GRAINE_API_KEY"
{ "id": "oex_2c9e8f1a7b3d4c5e6f7a8b9c", "status": "waiting", "node": "call",
  "wait": { "kind": "call", "node": "call", "until": "2026-10-26T11:10:00+00:00" },
  "variables": { "entry": { "mobile_number": "+919876543210", "name": "Asha", "plan": "gold" } },
  "timeline": [ { "at": "2026-10-26T10:10:00+00:00", "kind": "entered" } ] }

For a person of your app, send their ID in your app as user_id (and, when your app doesn't identify people by user_id, the field as user_field), and give the Call step "channel": "app" (In their app):

curl -X POST https://api.graine.ai/v2/workflows/owf_8a1d4e7c2b90f1a2b3c4d5e6/runs \
  -H "Authorization: Bearer $GRAINE_API_KEY" -H "Content-Type: application/json" \
  -d '{"contact": {"user_id": "cust-42", "name": "Asha", "preferred_time": "2026-10-28T18:00:00+05:30"}}'

While a call booked in the app waits for its time, the run's wait says so: {"kind": "call", "channel": "app", "due_at": "2026-10-28T12:30:00+00:00"}.

Saving a draft from code

PUT …/draft takes the whole definition and the revision you last read. If someone saved in between, it answers 409 and saves nothing: read the workflow again and apply your change to theirs.

Errors

The usual format, {"error": <code>, "message": "..."}:

  • 402 / 1400: your balance cannot fund a new run.
  • 409 / 1202, 1203: an agent the workflow calls is switched off, or was deleted.
  • 409 / 1201: a stale draft revision, a contact who already has a run under way, or a list that is not in a state to do that.
  • 422 / 1001: a required field is missing, or the workflow cannot be published. The message names each problem with its step.

On this page