Files
warmbly/docs/content/docs/guides/automations.mdx
T

174 lines
12 KiB
Plaintext

---
title: Automations
description: "A visual flow builder: a trigger event connected to action steps across your integrations."
---
An automation is a flow on a canvas: one **trigger** (a reply arrives, a meeting is booked) runs one or more **actions** (post to Slack, push to your CRM, tag a contact), with **IF conditions** in between to branch. No code, though an advanced mode accepts a free-form condition.
<Mermaid
chart={`
flowchart LR
T["Trigger"] --> C{"IF condition"}
C -->|yes| A1["Action"]
C -->|no| A2["Action"]
A1 -.->|on error| E["Error path"]
`}
/>
## The builder
Open the **Automations** tab. Each automation has a name, an on/off toggle, and a canvas. **Off** never fires, even from a campaign step.
- The **Trigger** node sits at the top and cannot be deleted.
- Drag a node's dot onto another node to connect, or onto empty canvas to pick what comes next (AI step, AI switch, condition, action, or **Stop**) already connected.
- Click a node to edit it. Click a connection and press `Delete` to remove it.
- **Add** places a node; **Tidy up** auto-arranges.
- Card positions save on their own, and the canvas is collaborative: teammates' cursors, live card movement, their selections outlined in their color. Press `/` to chat. See [Collaboration](/guides/collaboration/).
<Callout type="info" title="Save before it runs">
Triggers, conditions, and actions only take effect after **Save** (card positions are the exception). **Test** saves first, then dry-runs.
</Callout>
## Triggers
| Trigger | Fires when |
| --- | --- |
| Reply received | A contact replies to a campaign email |
| Contact created | A new contact is added by hand, through the API, by a form or by an automation (bulk file imports and sheet syncs stay quiet, see below) |
| Form submitted | A hosted [form](/guides/forms/) receives a submission |
| Meeting booked / rescheduled / canceled | A Calendly or Cal.com meeting changes |
| Email bounced | A campaign email bounces |
| Unsubscribed | A contact unsubscribes |
| Warmup health changed | A mailbox's warmup health state changes |
| Deliverability complaint | A spam complaint is recorded |
| Inbound webhook | An external system POSTs to this automation's unique URL |
| Campaign action | A campaign sequence reaches a "Run automation" step |
Each trigger carries its own event data (a reply carries contact email, reply intent, classifier confidence). Those are the values your conditions test and your action text inserts. Full per-trigger variable list: [Personalization & expressions](/guides/expressions/).
**Campaign action** never fires by itself; it runs only from a campaign step, still evaluating your conditions.
**Contact created** fires once per new contact, with the contact's fields and its first-touch source, so a condition can branch on `source` (a form, the API, an automation) or on any field. A contact that already existed and was only updated does not fire it. Bulk arrivals are deliberately silent: a file import or a Google Sheets sync of ten thousand rows would otherwise run ten thousand flows and flood every webhook, so those paths never raise it, and neither does a single API request adding more than 100 contacts.
### Inbound webhook
Pick the trigger and **save**, and the editor shows a unique URL. Anything that can send HTTP `POST`s JSON to it.
- The JSON body becomes the payload: `{"email":"a@b.com"}` gives you `{{.email}}`. A non-object body is exposed as `{{.body}}`.
- **The URL is the credential.** It carries a high-entropy token, so treat it as a secret, send it over HTTPS, and rotate it (switch the trigger away and back) if it leaks.
- The request returns immediately and the flow runs in the background, so a slow action never blocks the caller.
- Keys starting with an underscore are stripped; the body is capped at 1 MB.
## IF conditions
An IF node splits into a **yes** path (right dot) and a **no** path (bottom dot). Only the matching branch runs; an unconnected branch just ends, or drag it to a **Stop** node.
Pick a **field**, an **operator**, and a **value**. Fields depend on the trigger, so they are always meaningful.
| Field type | Operators |
| --- | --- |
| Text | `is`, `is not`, `contains`, `is present` |
| Number | `≥`, `≤`, `is`, `is present` |
| Choice (enum) | `is`, `is not`, `is present` |
Three special conditions work with any trigger:
- **Ask AI (yes/no)**: a plain-language question about the event ("Is this reply asking about pricing?") picks the path. Templated, so you can drop in `{{.field}}`. One credit per evaluation; if the model can't answer or you are out of credits, the no path runs and History shows why. See [AI steps in automations](/guides/ai-steps-in-automations/).
- **Random split**: a set percentage (`1%` to `99%`) down the yes path, deterministic per event so re-deliveries never flip it.
- **Advanced expression**: a free-form condition, taken when truthy.
```
and (gtf .confidence 0.8) (eq .intent "positive")
```
Comparisons (`eq`, `gt`, `lt`), numeric coercion (`gtf`, `ltf`, `add`, `sub`, `mul`, `div`), logic (`and`, `or`, `not`), and text helpers (`contains`, `lower`) are available. A broken or empty expression is **false**, so a malformed condition never silently passes, and it is validated on save.
## Actions
Each action node picks a **Run** target (an integration, or Warmbly built-in) and an **Action**.
| Integration action | What it does |
| --- | --- |
| Send a Slack / Discord message | Posts to a channel (Slack needs `#channel`) |
| Send a webhook | HTTP request to a URL you provide |
| Create / update HubSpot contact, Pipedrive person, Salesforce contact, Close lead | Upserts the record |
Slack, Discord, and webhook actions take an optional message template. Slack and Discord arrive as a branded card in Warmbly's accent color with contact and subject fields, not a plain line.
| Built-in action | What it does |
| --- | --- |
| Create or update contact | Makes a contact from the event's fields, or enriches the one with that email, then tags it and enrols it in a campaign. See [Lead intake](#lead-intake) |
| Add to campaign | Enrols the event's contact in a campaign |
| Add / remove a tag | Adds or removes a contact category |
| Label the email | Applies inbox labels to the replied-on conversation |
| Create a task | Assigned to the workspace owner |
| Create a deal | In a chosen pipeline and stage |
| Move the deal stage | Moves the contact's most recent open deal in that pipeline |
| Unsubscribe the contact | Only when the event carries a campaign |
| Set variables | Computes named values from templates for later steps to reuse as `{{.name}}` |
| Fire event | Publishes a `CUSTOM_EVENT` to the realtime gateway |
| AI step / AI switch | An agent or single-shot AI node, and AI-decided routing. See [AI steps](/guides/ai-steps-in-automations/) |
Three constraints worth knowing: **Move the deal stage** does nothing if the contact has no open deal in that pipeline, **Unsubscribe** needs an event carrying a campaign, and **Label the email** works only on a Reply received automation, since it needs a thread to label.
**Add to campaign** and every other contact action need a contact to act on: the event must carry `contact_id` or `contact_email`, which every Warmbly trigger does. On an inbound webhook, put **Create or update contact** first and the contact it writes becomes the event's contact for the steps after it.
### Lead intake
Any system that can send an HTTP request can create contacts in Warmbly without an API client: an **Inbound webhook** trigger followed by a **Create or update contact** action.
1. Pick the Inbound webhook trigger, save, and copy the URL.
2. Add **Create or update contact**. Each field is a template over the JSON the caller sends: an email of `{{.email}}`, a first name of `{{.first_name}}`, a custom field `team_size` set to `{{.answers.team_size}}`. Pick the tags and the campaign the lead lands in.
3. Point the sender at the URL. Zapier's *Webhooks by Zapier*, Make's *HTTP* module, n8n's *HTTP Request* node, a Typeform or Tally webhook, or your own code.
The action matches an existing contact by email, so re-sending the same lead updates it instead of duplicating it. A blank rendered value never erases a field the contact already has. **If the contact already exists** decides whether an existing contact is enriched and enrolled (the default) or left alone. New contacts carry `automation` as their first-touch source, with the automation's name as the detail.
<Callout type="info" title="Facebook, Instagram, LinkedIn and TikTok lead forms">
Meta, LinkedIn and TikTok deliver lead-form submissions to Zapier, Make and n8n in real time. Either route them through the Warmbly app there with its **Create or Update Contact** and **Add to Campaign** actions, or send them to an inbound webhook automation as above. Both end the same way: a contact with the ad's answers as custom fields, tagged and enrolled in the campaign that follows up from your mailbox. See [Zapier](/guides/zapier/#lead-forms-and-other-lead-sources), [Make](/guides/make/#lead-forms-and-other-lead-sources) and [n8n](/guides/n8n/).
</Callout>
Lead-form campaigns produce leads in bursts, and the follow-up still goes out from your mailboxes under their daily caps and spacing. A day of two hundred leads on one mailbox capped at fifty is a four-day send; the campaign's recipients-versus-capacity estimate shows it. Add mailboxes rather than raising caps.
A flow that creates a contact can be the reason a **Contact created** flow runs. That is allowed, and bounded: an event raised by an automation's own action carries how many hops led to it, and after five hops no further automation runs, so two flows cannot feed each other forever.
**Fire event** is the inverse of the inbound webhook: your app subscribes over the websocket with an API key holding `REALTIME_SUBSCRIBE` and receives `{ name, payload }`, so you get events without hosting a public URL. See [Realtime events](/api/realtime/).
### The on-error branch
Every action has a red dot on its right edge. Drag from it to handle failures (a rejected Slack message, a CRM timeout).
- **With** an on-error branch connected, a failure follows it and the run is **not** marked failed, like a try/catch. The normal path is skipped.
- **Without** one, the failure is recorded in History and the run is marked errored, but the flow continues down the normal path best-effort, so one bad step never blocks the rest.
- A dry run never fails an action, so **Test** always shows the normal path.
### Templating
Text fields (messages, webhook URLs, deal names, task titles) accept trigger variables as `{{.key}}`, with clickable chips in the builder to insert them:
```
New reply from {{.contact_email}} on {{.campaign_name}}
```
Unknown tokens render empty rather than failing.
## Testing and history
**Test** saves the canvas and dry-runs it against sample data: no messages sent, no records changed. It shows the exact path taken and a preview of each action. AI steps are the exception, running for real so you see actual output, which spends one credit per AI step.
**History** shows recent real runs: success or error, start time, and per-step results (error text on failure, a short summary on success). It updates live as the automation fires.
<Callout type="info" title="Best-effort by design">
Runs are best-effort and bounded against loops, so one failing step won't block the rest. The run is still recorded as errored so you can see what went wrong.
</Callout>
## Related guides
<Cards>
<Card title="Personalization & expressions" href="/guides/expressions/">
The condition and templating language shared by automations and campaigns.
</Card>
<Card title="Deliverability" href="/guides/deliverability/">
Warmup health and complaint signals that several triggers fire on.
</Card>
</Cards>