Graine AI

Steps

Every kind of step in an outreach workflow — Start, Call, Read call results, Check call history, HTTP request, Wait for your system, WhatsApp, SMS, email, Run an automation, Wait, Try again and End — what each does, its settings, and what it gives the steps after it.

Every workflow has one Start and at least one End. Between them, each step does one thing and then takes a path to the next. In the dashboard you pick a step from the palette and fill in its panel. Through the API, a step is one entry in the definition's nodes, with a type and a config.

SteptypeDoesGives the next steps
StartstartWhere a contact entersThe contact, as entry.*
CallagentCalls the contact with one of your agents, by phone or in your appHow the call went, as call.*
Read call resultsextractionReads what the agent's post-call analysis found in that callThe fields you picked, as extraction.*
Check call historyhistoryCounts the calls already placed to the person lately, by any of your agentsHow often they were called and answered, as history.*
HTTP requestapiSends one HTTP request to your systemIts answer, as response.*
Wait for your systemcallbackWaits until your system calls back, or a timeoutWhat your system sent, as callback.*
Send a WhatsApp messagewhatsappSends an approved template, or a text, on your connected WhatsApp numberHow it went, as message.*
Send an SMSsmsSends a text message with your SMS connectionHow it went, as message.*
Send an emailemailSends an email with your email connectionHow it went, as message.*
Run an automationautomationStarts one of your automations with the run's valuesWhether it started, as automation.*
WaittimeWaits for a while, or until a time—
Try againretryGoes back to an earlier step later, a limited number of times—
EndendEnds the run with a label and an outcome—

Start

The fields every contact brings. mobile_number, name, email, reference_id, user_id (the person's ID in your app) and user_field (which of your app's fields that ID is in: user_id, email, phone or your own) are always accepted; add your own, each with a type and whether it is required: string, number, boolean, timestamp, phone, email, url or json. A value is converted to its type as the contact enters ("50,000" becomes 50000). A contact missing a required field is refused there and then, before anything is called.

reference_id is how a contact is told apart: a contact can only have one run of a workflow under way at a time. Leave it out and the phone number is used, or the user ID for someone with no number.

{ "id": "start", "type": "start", "config": { "fields": [
  { "name": "plan", "type": "string", "required": true },
  { "name": "amount_due", "type": "number" }
] }, "cases": [ { "when": { "always": true }, "then": { "to": "call" } } ] }

Call

Calls the contact with one of your agents, then waits for the call to end. Under How to reach them, choose a Phone call (the default) or In their app: the person's phone rings in your app, as a call does (In their app below).

SettingMeaning
Agent (agent_id)Any of your agents, prompt or flow.
How to reach them (channel)phone (the default) or app.
Number to call (recipient)A phone call: a value to call, entry.mobile_number unless you choose another.
Caller ID (from_number)A phone call: the number the person sees, picked from your Phone numbers and stored exactly as that page lists it. Leave it to us and one of your numbers is chosen, as for any call you place through the API.
Reason (reason)Optional, up to 120 characters: why the agent calls. The agent hears it as call_reason; values go in as {placeholders}.
Wait for the call (timeout_s)How long to wait for the call to end once it is dialled (in the app: once it rings): 1 minute to 1 day, 1 hour by default.
Values for the agent (variables)Extra values for the agent's prompt. The contact's fields are always sent, except the user ID unless you add it here. Your agent uses lists the {variables} in the agent's prompt and greeting (a flow agent's values that come with the call), and says which the run sends; Add … to the contact makes the missing ones contact fields, which a test run, a list and your system then fill in.

How the call went is in call.status: completed, no_answer, busy, voicemail, failed, cancelled, timeout (it did not end in time) or error (it could not be placed: no number, or the agent was deleted). With it come call.duration_s and call.execution_id, the call's id in Call History.

A call held back is waited for, not failed

When your balance is empty, the agent is switched off, it is outside the agent's calling hours or every line is busy, the call waits and goes out as soon as it can. Waiting is never an attempt, and the timeout only counts from the moment the call is dialled. A call that has waited 7 days without being dialled is given up as error.

In their app

With In their app, the step rings the person your app has signed in (Identity and users), on their phones that use your app, with hosted ringing. It can ring them now, or book the call for a time, as a scheduled call.

SettingMeaning
Who to ring (identity)The value that holds the person's ID in your app, entry.user_id unless you choose another.
Their ID's field (identity_field)Which of your app's fields that ID is in: the one the agent's Memory & identity setting uses (user_id, email, phone or your own). The dashboard fills it in from the agent. Leave it out and the contact's own user_field is used, else user_id.
When it rings (at)Leave it out to ring now. Or a date-and-time value, like entry.preferred_time, or a wait, like {"delay": {"hours": 2}} (at most 30 days). A value that is empty for someone, or a time already past, rings now.
Time zone (timezone)Optional: the zone of a time written without its offset. Without one, the person's own time zone is used.
{ "id": "call", "type": "agent",
  "config": { "agent_id": "agt_0123", "channel": "app", "identity": "entry.user_id",
              "at": "entry.preferred_time", "reason": "About your order", "timeout_s": 1800 },
  "cases": [ { "when": { "cmp": "==", "left": { "var": "call.status" }, "right": { "const": "completed" } },
               "then": { "to": "results" } } ],
  "on_no_match": "not_reached" }

A call in the app follows every rule of a call your app or your agent books: it rings only within the agent's calling hours, outside quiet hours, in the person's own time zone, and a booked time outside them moves to the next time that works. A booked call shows in the person's upcoming calls in your app, and in the agent's Scheduled calls. The timeout counts from when it rings, so a call booked for tomorrow is waited for from tomorrow. If you cancel the run, a call it booked that has not rung yet is cancelled too; a call that is ringing or live is never cut.

How it ended is in call.status, the same values as a phone call:

In the appcall.status
Answered and talkedcompleted, with call.duration_s and call.execution_id, the conversation's id in Call History
Declined on their phonebusy
Rang and nobody answered (a second ring included, when the agent rings a missed call again)no_answer
Rang, but couldn't ring again in time (rung too often today, declined a call recently)no_answer, with the reason in call.error
Its time passed before it could ring at all (more than 10 minutes late)failed, with call.error expired_unrung — nobody was asked, so it isn't "not answered"
No phone of theirs could be rung, or the ring failedfailed, with the reason in call.error (no_device when none could be rung)
Cancelled from Scheduled callscancelled

When the call can never be placed as written, call.status is error and call.error says why: no_identity (no ID for this person), invalid_identity (an ID that names nobody), no_app_user (none of their phones has used your app in 30 days), app_calls_not_set_up (your app can't be rung yet: see Hosted ringing), too_soon (a booked time sooner than the agent lets calls be booked) or invalid_time. An empty balance, a switched-off agent, quiet hours, too many calls already booked for the person, or a time further ahead than the agent books calls don't fail the step: the call is asked for again later, and that is never an attempt. A booked time is fixed when the step first runs, so asking again never moves it later; a time that only became too soon while it waited moves to the soonest the agent allows. Calls booked into quiet hours ring when they end, spread over two minutes so a list of them doesn't ring at the same second.

One call, even when it reconnects

A call in the app that reconnects partway through is still one call. The step waits until the whole call is over, then reads the conversation that was really had.

Read call results

Reads the fields your agent's post-call analysis (Agent → Custom Analysis) found in the call its Call step placed. Put it right after that Call step, and pick the fields you want.

{ "id": "facts", "type": "extraction",
  "config": { "agent_node_id": "call", "selected": ["interested", "callback_time"] },
  "cases": [ { "when": { "cmp": "==", "left": { "var": "extraction.interested" }, "right": { "const": true } },
               "then": { "to": "crm" } } ],
  "on_no_match": "not_interested" }

The analysis is ready a minute or so after an answered call, and the step waits for it, at most 10 minutes. For a call nobody answered there is nothing to read, so the step moves on at once with no fields. Set to The call or chat that started the run, it reads the call or the chat whose end started the run (Start when a call ends).

Check call history

Counts how many times the person was already called in a window before now, by any of your agents: lists, other workflows, scheduled calls and callbacks, and the API all count, and so do this run's own earlier calls. Put it before a Call, or make your Try again go back to it, and its paths decide whether to call: stop at three calls a week, stop once someone has answered, or go on. Nothing else changes: only a workflow with this step looks at the history.

{ "id": "check_history", "type": "history",
  "config": { "window": { "days": 7 } },
  "cases": [
    { "when": { "cmp": ">=", "left": { "var": "history.calls" }, "right": { "const": 3 } }, "then": { "to": "called_enough" } },
    { "when": { "cmp": "==", "left": { "var": "history.status" }, "right": { "const": "unavailable" } }, "then": { "to": "not_checked" } }
  ],
  "on_no_match": "call" }
SettingMeaning
Whose calls (channel)phone (the default): calls to their phone number. app: calls that rang them in your app.
Their number (match)By phone: the value that holds their number, entry.mobile_number unless you choose another.
Their ID in your app (identity, identity_field)In the app: the same as a Call in their app — the value holding their ID, and the field it is in.
How far back (window)Whole days or hours, from 1 hour to 90 days: {"days": 7}, {"hours": 12}.

The person is found by their number (or, in the app, their ID in your app), never by the contact: the same person is a different contact in each list, and every one of those calls counts. Only calls that really went out count: not a call that was booked and never rang, or one refused before it was placed (an empty wallet, an agent switched off).

ValueMeans
history.callsCalls placed to them in the window.
history.answeredOf those, the ones a person answered — not a voicemail.
history.last_called_atWhen the latest of them was placed.
history.last_statusHow it ended, in the words of call.status: completed, busy, no_answer, failed, voicemail or cancelled. Empty while that call is still going on.
history.statusok, or unavailable when the history couldn't be read just then.
history.errorWhy it is unavailable: no_person (no number, or no ID in your app, to look up), timeout or unreadable.

Give “unavailable” a path

When the history can't be read, history.calls is empty, so a rule like "at least 3" is false and the run would take Otherwise — the call. Add a path for history.status is unavailable that ends the run, or waits and checks again, so nobody is called without being counted.

HTTP request

One HTTP request to your system: push a qualified lead to your CRM, look a customer up, book a slot. Setup is in Connect your systems. The answer is in response.status and in the values you map from its body. When your system could not be reached, there is response.error. A 4xx or 5xx is an answer like any other, so a path can act on it.

Wait for your system

Pauses the run until your system calls back, for example when a deal changes stage or a payment arrives, or until the timeout. How your system calls back is in Connect your systems.

SettingMeaning
How to find the contact (correlation.payload_path)Where your request names the contact, such as lead.phone.
Compared with (correlation.matches)The contact's value it must equal, entry.mobile_number unless you choose another.
Values to keep (payload_map)Names for values in your request, such as stage from lead.stage.
Require a signature (require_signature)Accept only signed requests.
Wait up to (timeout_s)1 minute to 30 days, 1 day by default.

callback.status is received or timeout, with your values beside it. A path must say what happens on timeout.

Send a WhatsApp message

Sends a message from one of your connected WhatsApp numbers (Integrations → WhatsApp). Choose an approved template, with its language and the values for its placeholders, or a text. A text only reaches someone who wrote to your number in the last 24 hours; to start a conversation, use a template.

{ "id": "nudge", "type": "whatsapp", "config": {
    "channel_id": "your-whatsapp-channel",
    "template": { "name": "payment_reminder", "language": "en",
                  "parameters": [ { "var": "entry.name" }, "{amount}" ] },
    "variables": { "amount": { "value": { "var": "entry.amount_due" } } } },
  "cases": [ { "when": { "cmp": "==", "left": { "var": "message.status" }, "right": { "const": "sent" } },
               "then": { "to": "done" } } ],
  "on_no_match": "not_sent" }

message.status is sent or failed, with message.id, or message.error saying why: no number to send to, the channel needs reconnecting, or the code WhatsApp gave.

Send an SMS and Send an email

Send with one of your messaging connections (see Connect your systems): an SMS with Twilio, an email with SendGrid or Resend. An SMS is up to 1,600 characters, sent to entry.mobile_number unless you choose another value. An email has a subject and a text, sent to entry.email unless you choose another. Both can use placeholders, and both set message.* as WhatsApp does.

Run an automation

Starts one of your automations (Workflows → Automations) with the values you choose from the run. That brings everything an automation can do into a workflow: Slack, HubSpot, Google Sheets, Notion, Salesforce, Stripe and the rest. automation.status is started or failed; the automation then runs on its own.

Wait

Waits for a time (delay: days, hours and minutes, up to a year) or until a moment (at). The moment can be a fixed time with its offset, or a value of type timestamp with a time zone, such as a follow-up time your system sends with the contact. It has one way out, its to.

{ "id": "until_follow_up", "type": "time",
  "config": { "at": { "var": "entry.follow_up_at" }, "timezone": "Asia/Kolkata", "to": "call_again" } }

Try again

Goes back to an earlier step, such as the Call, after a wait, for a limited number of attempts: 1 to 10, each with its own wait. It is the only way a workflow can go round in a loop. Once every attempt is used, its paths decide what happens next, usually an End.

When it goes back to a Call, the next call is booked for its time straight away. Until it rings, it is under Upcoming in Call History, marked Booked by a workflow, and it meets the same checks as any call when its time comes: your balance, the agent being on, its calling hours and your lines. It carries the same values the step would send at that time. If you cancel it there, the run moves on with call.status cancelled. A call in your app that has no time of its own is booked the same way, in the person's upcoming calls.

{ "id": "again", "type": "retry",
  "config": { "target_node_id": "call", "attempts": [ { "delay": { "minutes": 30 } }, { "delay": { "hours": 4 } } ] },
  "cases": [ { "when": { "always": true }, "then": { "to": "unreached" } } ] }

End

Ends the run with a label, the name you will count it by (qualified, paid, unreached), and an outcome: success, failure or neutral. Each published version counts its runs by outcome, and every list shows its totals.

On this page