Graine AI

Connect your systems

Send a run's results to your CRM, pause a run until your system answers, start runs from your system with the trigger URL or when a call ends, connect WhatsApp, SMS and email, and sign and check requests.

A workflow talks to your systems in three ways: an HTTP request step sends them a request, a Wait for your system step waits for them to call back, and the trigger URL lets them start a run. The editor's Connect tab shows three ways to start a run — the trigger URL, Start when a call ends and the API — then the callback URL, the signing secret and your messaging connections. The workflow's Connect panel in the dashboard shows its URLs and its signing secret.

HTTP request

One request: method (POST unless you choose GET, PUT, PATCH or DELETE), URL, headers and a JSON body, with values from the run in placeholders (see Paths and data).

{ "id": "crm", "type": "api", "config": {
    "method": "POST",
    "url": "https://crm.example.com/leads",
    "headers": { "Authorization": "Bearer {crm_token}" },
    "body": { "phone": "{phone}", "interested": "{interested}", "resume_url": "{resume_url}" },
    "variables": {
      "crm_token": { "value": { "const": "your-crm-token" }, "required": true },
      "phone": { "value": { "var": "entry.mobile_number" }, "required": true },
      "interested": { "value": { "var": "extraction.interested" }, "type": "boolean" },
      "resume_url": { "value": { "var": "run.callback_url" } }
    },
    "response_map": { "lead_id": "data.id" },
    "timeout_s": 30
  },
  "cases": [ { "when": { "cmp": "<", "left": { "var": "response.status" }, "right": { "const": 300 } },
               "then": { "to": "wait_crm" } } ],
  "on_no_match": "crm_failed" }
  • The URL's host is written out in full, never taken from a value, and must be a public address. Requests to private or local addresses are refused.
  • The answer can be up to 256 KB. response_map reads values out of it by dotted path (data.items.0.id reads into a list) and makes them response.<name>.
  • response.status is the HTTP status. A 4xx or 5xx is an answer, so a path can act on it; nothing is retried behind your back. When your system could not be reached in time, response.error is transport.
  • Redirects are followed only when you turn Follow redirects on, at most 3, each to a public address.
  • Headers and values are saved with the workflow, and anyone who can view the workflow can read them. Use a token made for this one job, which you can revoke on its own.

Wait for your system

The run pauses until your system posts to one of its callback URLs:

URLUse it when
The run's own, run.callback_urlYour system can be told the URL. Send it in an earlier request, as resume_url is above, and post back to it.
The workflow's, in its Connect tabYour system has one webhook setting for everything, such as a CRM's "when a deal changes, call this URL". The body names the contact, and every run of this workflow waiting for that contact is resumed.

Either way, the body must name the run's contact at the step's correlation.payload_path, compared with correlation.matches (the phone number by default; phone numbers are compared on their last ten digits). The values you list in payload_map become callback.<name>:

curl -X POST "https://api.graine.ai/v2/workflow-callbacks/<token>" \
  -H "Content-Type: application/json" \
  -d '{"lead": {"phone": "+919876543210", "stage": "won"}}'
{ "resumed": 1 }

resumed is 0 when no run is waiting for that contact. The step's timeout gives callback.status = "timeout" instead, and a path decides what happens then.

Trigger URL

The trigger URL starts a run for each contact your system posts, with no API key. It is off until someone whose starts need no approval (in most workspaces, an admin) turns it on in the Connect tab.

Connect: three ways to start a run — Trigger URL, Start when a call ends, API — and the trigger URL with Copy and Rotate, Require a signature, the version it starts, and the signed request to copy

There you can also pin a version, so publishing again doesn't change what your system starts, and require a signature.

curl -X POST "https://api.graine.ai/v2/workflow-triggers/<token>" \
  -H "Content-Type: application/json" \
  -d '{"contact": {"mobile_number": "+919876543210", "name": "Asha", "plan": "gold"}}'
{ "execution_id": "oex_2c9e8f1a7b3d4c5e6f7a8b9c", "status": "queued" }

The contact can also be the whole body. A trigger URL starts at most 120 runs a minute. Before each start, your balance and the workflow's agents are checked, exactly as for a run started in the dashboard.

Start when a call ends

Turn on Start when a call ends in the Connect tab, and a run starts by itself whenever a call — or a chat — with the agents you choose ends the way you choose:

Start when a call ends: agents to choose, call results (Answered, Not answered, Busy, Voicemail, Failed), directions (Calls you made, Calls you received) and the version it starts

SettingChoose
AgentsOne or more of your agents
What countsCalls, Chats, or both (Calls unless you choose)
Call resultsAnswered, not answered, busy, failed, voicemail
DirectionsCalls you made, calls you received (phone calls), In the app (calls in your app, whichever way), On the website (voice conversations on your website, or in your app's voice bar)
VersionThe version that runs, or the latest

The run is for the person on the other end. On a phone call that is their number: the caller of an inbound call, the person called on an outbound one, as entry.mobile_number. In your app, on your website and in a chat it is the user your app signed in, by the ID the agent knows them by (its Memory & identity setting): entry.user_id, with the field it is in as entry.user_field. A call or a chat with nobody signed in starts nothing — the run would not know whom it is for. A call in your app that Graine rang starts its run when the ring ends: answered and over, declined, or not answered.

The call that ended is trigger.call, with its status, direction, duration and id; a chat that ended is trigger.chat, with its id, how many messages were written and how long it lasted. A Read call results step set to The call or chat that started the run reads what the agent's analysis found in it. Use it for a missed call that should be called back, for a sales call whose interested callers should get a WhatsApp follow-up, or for a chat that asked for a call (After a chat, follow up).

Each call or chat starts its run once. Whether it was a chat or a call is the conversation's own — never which of its updates reached Graine first — and a call in your app that reconnected partway through (from one connection to another) starts its run once, from the conversation that was really had: the longest. A call placed by a workflow never starts another run, so there are no loops. A person who already has a run of the workflow under way doesn't get a second one. Like the trigger URL, it is turned on by someone whose starts need no approval.

Recent calls that matched, under the settings, lists the latest ten calls and chats that matched, newest first: the person (a number's last four digits), how the call ended, which way and with which agent, and either the run it started or why it started none. That happens when the person already has a run of the workflow under way, an agent the workflow calls with is switched off, the wallet is empty, or the Start step requires a field the call doesn't carry. Open the run opens it in Runs. A call that doesn't match (another agent, result or direction) isn't listed.

In the list of workflows, the Trigger column shows every way a workflow starts by itself: When a call ends, Trigger URL and From your app.

Through the API, the same settings are the workflow's call_trigger: directions takes inbound, outbound, app and web, and channels takes call and chat (["call"] when it isn't set).

Before a call is placed

Hold an agent's calls to someone who has already had enough of them. Turn it on, choose the agents, and set the limit — for example, at most 3 calls in the last 2 days. From then on, every call those agents would place is checked the moment it would be placed, whoever places it: a campaign or a list (and their retries and follow-ups), the API, a scheduled call or a workflow.

  • What counts: every call placed to the person's number in that time, by any agent in your workspace, answered or not — the same count as the Check call history step. Calls the person made to you don't count.
  • A call over the limit waits. It goes out once the person can be called again. It is never marked failed and never counted as an attempt:
    • a campaign or list contact waits its turn until then, and the campaign carries on with everyone else;
    • a retry or a follow-up keeps its number and is placed then;
    • a scheduled call, and a workflow's Call step, wait — the run waits with it;
    • a call you place now with POST /v2/calls is queued too: the answer is 202 with status: "scheduled", reason: "held_by_call_limit" and the scheduled_at it will be placed at — nothing to resend. Sent again, it isn't queued twice: while that number has a call waiting with that agent, the answer is the waiting call (already_queued: true). Only if it can't be queued do you get error 1204;
    • a test call — from an agent's page, or one Sprout places — is never held: someone is waiting on it. It still counts towards the number's calls.
  • Calls at night. A held call goes out at the first moment it's allowed. A campaign's calls keep to the campaign's calling hours; the API's, scheduled calls and a workflow's keep to the agent's Calling window when it has one — give your agents one, so a call that frees up at night waits for the morning.
  • A campaign's end date still applies. A contact, retry or follow-up still waiting when its campaign ends is not called: it closes as skipped, with that reason. Give a campaign enough days for the limit to pass.
  • Everywhere in your workspace. It holds the chosen agents' calls wherever they come from, not only this workflow's. When two workflows choose the same agent, both limits apply.
  • The window is from 1 hour to 7 days.

Turn it off, or remove an agent, and calls waiting for it go at their next turn.

Start from your app

Your app can start a workflow for the user it signed in, with the in-app SDK and your publishable key — no server of yours. Turn on Your app can start this in the workflow's Connect tab (an Admin does), choose the fields your app may send, and call startWorkflow from the app:

const { runId } = await client.startWorkflow("Welcome call", { plan: "gold" });
SettingChoose
Your app can start thisOn or off (off unless an Admin turns it on)
Fields your app may sendThe contact fields the app may fill in: name and the Start step's own fields — never a phone number, an email address or a field a step sends to
VersionThe version that runs, or the latest

The run is always for the user your app signed in, by the ID the agent knows them by (entry.user_id): an ID your app puts in its data is ignored, and so is anything else the workflow doesn't allow. Each value is text (at most 200 characters), a number or yes/no, and the app may send at most 12. A user who already has a run of the workflow under way gets that run back, not a second one, and a retry with the same key never starts two.

Your app never chooses where a run sends. mobile_number, email, any field a call, SMS, WhatsApp or email step sends to, and any Start field that is a phone number or an email can't be fields your app may send, and a start never takes them from what the app sends. When the agent knows its users by phone or email (its Memory & identity setting) and the user is verified, the run reaches them at that phone number or address. Otherwise reach people from your own records — an API step that looks them up — or with a call in your app.

A start passes the same checks as everything your app does with the agent: your apps, a verified app where you require one, and — with Require verified users on — a verified user. It also needs the user signed in on one of their own devices (the agent's Devices per user) unless they are verified, and your balance and the workflow's agents are checked as for any start. Starts are limited per device, per user and per workflow. What the app gets back, and every error, is in the SDK reference. Requires SDK 0.14.4.

Messaging connections

WhatsApp messages go out on the numbers you connected under Integrations → WhatsApp. Nothing more to set up.

SMS and email use a messaging connection: add one under Manage connections in the Connect tab (the SMS and email connections dialog), choosing Twilio for SMS (account SID, auth token and your Twilio number) or SendGrid or Resend for email (an API key and the address you send from). The credentials are encrypted when you save them and are never shown again. Only workspace admins add or remove connections.

SMS and email connections: an SMS connection and an email connection, each with its provider and the number or address it sends from, and buttons to add more

Signing requests

Turn on Require a signature (on the trigger, or on a Wait for your system step) and only signed requests are accepted. The signature is an HMAC-SHA256 of the raw request body with the workflow's signing secret, sent as X-Graine-Signature: sha256=<hex>.

import crypto from "node:crypto";
 
const body = JSON.stringify({ contact: { mobile_number: "+919876543210", plan: "gold" } });
const signature = "sha256=" + crypto.createHmac("sha256", process.env.GRAINE_SIGNING_SECRET)
  .update(body).digest("hex");
 
await fetch(process.env.GRAINE_TRIGGER_URL, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Graine-Signature": signature },
  body, // send exactly the bytes you signed
});

Treat the URLs and the secret like passwords

Anyone with the trigger URL can start runs (and spend your credit), and anyone with a callback URL can answer that run. Keep them in your server's configuration, never in a web page or an app. If one leaks, Rotate it in the Connect tab: the old one stops working at once.

Answers

Both URLs answer in the API's error format, {"error": <code>, "message": "..."}:

StatuserrorMeans
4001000The body is not a JSON object, or it is over 256 KB
4011100The signature is missing or wrong
4021400Your balance cannot fund a new run (trigger)
4041200No such URL, or the trigger is off
4091201No run is waiting there (the run's own URL), or the contact already has a run under way (trigger)
4091202, 1203An agent the workflow calls is switched off, or was deleted (trigger)
4221000The body names a different contact from the waiting run, or lacks a required field (trigger)
4291300More than 120 starts in a minute (trigger)

On this page