Skip to content
Elara One docs

Flows

Automation flows from a developer's point of view, including triggers, actions, the webhook action, loop protection, consent and the merchant API.

A flow is an automation the merchant builds in the store admin under CRM → Automation → Flows: a trigger, then checks (yes and no paths), waits and actions. Most stores never touch flows through code. This page is for developers who connect flows to their own systems, or create flows through the merchant API.

Triggers

A flow starts on one of 17 events. Filters narrow it down, for example to one form or one tag. Filter values are compared as strings.

EventStarts whenFilters
form.submittedA store form is submitted.form_handle
tag.addedA tag is added to a contact (only when it is new to them).tag
message.receivedA message arrives in the inbox.channel (sms, web)
order.placedAn order is placed.
order.cancelledAn order is cancelled.
order.dispatchedAn order is dispatched.
order.deliveredAn order is marked delivered.
call.completedAn AI voice call ends (coming soon, with AI voice).
analytics.alert_firedAn analytics alert fires (coming soon, with Analytics).alert_id, metric, rule
crm.stage_changedA contact's or company's lifecycle stage changes.object, from, to
crm.field_changedA CRM write changes fields (one run per write).object, field, value
crm.owner_assignedA record gets an owner.object, owner
task.overdueA task becomes overdue.type, queue
crm.no_replyNo reply N hours after the store's last message, or a lead's response target passes.after, hours, channel, source
company.createdA company is created.
list.joinedA contact joins a smart list.list
list.leftA contact leaves a smart list.list

The order.* triggers need the Shop. Every other trigger, apart from analytics.alert_fired, needs the CRM, which every store has.

Actions

ActionDoesMain settings
send_emailEmails the contact, or for an order trigger the order's contact address.subject, body, recipient (customer or order)
send_smsTexts the contact through the store's Twilio number.body
send_templateSends one of the store's message templates.template (its key)
webhookPosts the run's data to your HTTPS endpoint.url
create_taskCreates a CRM task.title, type, priority, due_in_days, assignee, queue
set_stageMoves the contact or company to a lifecycle stage.stage, object, only_forward
set_fieldSets a standard or custom field.field, value, object
set_ownerAssigns an owner.owner, object, only_if_unowned
add_tagAdds a tag.tag
remove_tagRemoves a tag.tag
add_noteAdds a note to the timeline.body, object
vapi_callPhones the customer with an AI assistant (coming soon).

Text in emails, texts, tasks and notes can use placeholders such as {{ crm.company_record.name }}. WhatsApp templates in send_template are coming soon, with WhatsApp in the inbox.

The webhook action

The webhook action posts JSON to a URL you choose. Use it to tell your own system that something happened: a quote request to price, a VIP to call back.

  • The URL must be https:// and must resolve to a public address. Private, loopback and reserved addresses are refused, as is a host that does not resolve.
  • Redirects are not followed. A 3xx answer counts as a failure, like a 4xx or 5xx.
  • The body has event (flow.webhook), customer_id, trigger (the event and its data) and vars (what earlier steps produced). For a form trigger, the trigger data includes the form's answers, so treat the body as personal data.
  • The request carries X-ElaraOne-Signature: the lower-case hex HMAC-SHA256 of the raw body, keyed with the store's server secret (the X-Tenant-Secret value). There is no timestamp in this signature.
  • A staging store's requests also carry X-ElaraOne-Environment: staging.
Verify a flow webhook (Node)
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyFlowWebhook(rawBody: string, header: string | null, serverSecret: string): boolean {
  if (!header) return false
  const expected = createHmac('sha256', serverSecret).update(rawBody, 'utf8').digest('hex')
  const a = Buffer.from(expected, 'utf8')
  const b = Buffer.from(header, 'utf8')
  return a.length === b.length && timingSafeEqual(a, b)
}

A post with no answer, or an answer outside 2xx, fails the step, and the run's page in the admin shows the error under "For developers". Answer quickly and do the work afterwards. For logged delivery of CRM changes with retries and a "Send again" button, use CRM webhooks instead.

Loop protection

A flow that changes a field could start itself again for ever, and two flows could set each other off. The engine stops both:

  • a flow never starts from a change it made itself, or from a change made by a flow earlier in its chain (A → B → A stops at the second A);
  • a chain of flows stops at a depth of 3, however many different flows take part.

A blocked start is logged and skipped. Nothing fails.

Flows respect the store's do-not-contact list and the contact's marketing consent.

  • send_email to the contact and send_sms are automated messages. An address or number on the do-not-contact list is skipped: the step succeeds with skipped: "recipient_suppressed", so the rest of the flow still runs. Every automated email carries a one-click unsubscribe.
  • send_email with recipient: "order" is a service message about an order the person placed, like a dispatch notice. It is allowed only on order.* triggers.
  • send_template follows the template's purpose. A Marketing template needs a current "yes" to marketing consent and an address not on the do-not-contact list; a contact who never answered gets nothing. A Service template skips the marketing checks.
  • A text that answers the person's own incoming SMS, sent back into the same conversation soon after, counts as a reply, not automation.

Test with a contact

In the flow builder, Test with a contact walks the draft for a real contact, using their latest form answers or the trigger's own details. Each check is worked out, each action says what it would do, and each wait says when the run would carry on. Nothing is changed, sent or queued, and no run is recorded.

Each published version is kept, and a run always finishes on the version it started with, so publishing a change never disturbs runs already under way.

Merchant API

A merchant API key with the manage_flows ability can list and save flows.

MethodPathNotes
GET/v1/merchant/flowsPaginated. filter[handle], filter[is_enabled], sort=name|handle|created_at.
POST/v1/merchant/flowsCreate or update by handle.

A flow document has a trigger (event and filters) and a graph of nodes. Each node has an id, a type (trigger, condition, action, delay or end), a config and a next. A condition node's next is { "true": "…", "false": "…" }; every other node's next is a node id or null. There must be exactly one trigger node and no cycles.

curl -X POST https://api.elaraone.app/api/v1/merchant/flows \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "Authorization: Bearer $ELARAONE_MERCHANT_KEY" \
  -H "Content-Type: application/json" \
  -d @flow.json
flow.json
{
  "handle": "quote-to-crm",
  "name": "Quote requests to our pricing tool",
  "is_enabled": false,
  "trigger": { "event": "form.submitted", "filters": { "form_handle": "quote-request" } },
  "graph": {
    "nodes": [
      { "id": "trigger", "type": "trigger", "config": {}, "next": "tag" },
      { "id": "tag", "type": "action", "config": { "action": "add_tag", "tag": "quote" }, "next": "notify" },
      { "id": "notify", "type": "action", "config": { "action": "webhook", "url": "https://pricing.example.com/hooks/quote" }, "next": "end" },
      { "id": "end", "type": "end", "config": {}, "next": null }
    ]
  }
}

The answer is the saved flow: id, handle, name, trigger, graph, is_enabled, version, created_at and updated_at.

A save through the API is live at once

A flow saved through the API is published immediately and kept as a version. is_enabled defaults to true, so send false until you have tested it in the builder.

The document is checked by the same validator as the flow builder. A problem is a 422 VALIDATION_FAILED with details keyed by where it is: trigger, graph, graph.<node id> or graph.<node id>.config.<setting>. A key without the ability gets 403 FORBIDDEN.