Templates
list_email_templates, get_email_template, create_email_template, update_email_template, delete_email_template, preview_email_template, validate_email_template, and send_test_email_template.
HAL email campaigns are a high-level MCP facade over visitor workflows. Use them for onboarding, activation, and follow-up emails without manually editing workflow nodes.
Campaign tools create and manage HAL visitor workflows with send_email nodes. Template tools store reusable email templates in the selected project's widget config, so branding and content stay isolated per project.
project_id or project_slug.widget_config.workflow_email_templates.workflow_id for traceability.enabled: true is explicitly passed.list_email_templates, get_email_template, create_email_template, update_email_template, delete_email_template, preview_email_template, validate_email_template, and send_test_email_template.
list_email_campaigns, get_email_campaign, create_email_campaign, update_email_campaign, enable_email_campaign, disable_email_campaign, delete_email_campaign, and send_test_email_campaign.
list_email_campaign_runs and get_email_campaign_run return sent, failed, skipped, opened, clicked, clicked URL, subject, recipient, chain source, and error details from workflow execution history.
list_event_names and list_custom_properties help clients find tracked lifecycle events, visitor properties, company fields, and company metadata keys before creating trigger rules.
list_email_suppressions, suppress_email_recipient, unsuppress_email_recipient, and get_email_unsubscribe_status manage project-level suppression.
This sequence creates a template, sends a safe internal test, creates a delayed welcome campaign, enables it, and reads delivery runs.
create_email_template({
project_slug: "space-invoices",
name: "Welcome onboarding",
subject: "Welcome to {{project_name}}",
preheader: "A quick setup path for {{visitor_name}}",
blocks: [
{ "id": "h1", "type": "heading", "level": 1, "text": "Welcome, {{visitor_name}}" },
{ "id": "p1", "type": "paragraph", "text": "Here are the first steps to get value from {{project_name}}." },
{ "id": "cta", "type": "button", "text": "Open setup", "url": "https://example.com/setup" }
]
})
preview_email_template({
project_slug: "space-invoices",
template_id: "workflow-email-template-..."
})
send_test_email_template({
project_slug: "space-invoices",
template_id: "workflow-email-template-...",
to_email: "founder@example.com"
})
create_email_campaign({
project_slug: "space-invoices",
name: "Welcome one day after first seen",
template_id: "workflow-email-template-...",
trigger: { "type": "first_seen" },
delay_seconds: 86400,
reevaluate_conditions_at_send: true,
frequency: "once_ever"
})
enable_email_campaign({
project_slug: "space-invoices",
campaign_id: "..."
})
list_email_campaign_runs({
project_slug: "space-invoices",
campaign_id: "..."
})| Trigger | Use it when | Required fields |
|---|---|---|
first_seen |
A visitor should receive a message after first identification or first visit. | type |
event |
A tracked lifecycle event should start the campaign. | type, event_name |
page_visit |
A visitor should qualify after visiting a URL or URL pattern. | type, pattern, optional match_type |
property_match |
Visitor, contact, company, or event data should qualify the recipient. | type, conditions or predicates |
no_event_after |
A follow-up should send only if an expected event has not happened within a window. | type, anchor_event, missing_event, within_seconds |
Use this trigger for gaps such as "account created but no invoice issued in 24 hours." For workspace-level onboarding, scope both the trigger and frequency to the company.
create_email_campaign({
project_slug: "space-invoices",
name: "No first invoice after account creation",
template_id: "workflow-email-template-...",
trigger: {
type: "no_event_after",
anchor_event: "account.created",
missing_event: "invoice.issued",
within_seconds: 86400,
scope: "company"
},
frequency: "once_ever",
frequency_scope: "company",
conditions: [
{ field: "company_metadata.plan", operator: "equals", value: "trialing" },
{ field: "company_metadata.first_invoice_sent", operator: "is_false" }
]
})Use next_campaign_id when a successful email should trigger another campaign instead of building a multi-step sequence inside one campaign. HAL runs the next campaign only after the current email is sent. Failed, skipped, or suppressed sends do not trigger the next campaign.
// Create the follow-up first, then reference it from the earlier campaign.
create_email_campaign({
project_slug: "space-invoices",
name: "Setup reminder",
template_id: "workflow-email-template-reminder",
trigger: { type: "event", event_name: "setup_reminder.manual" },
frequency: "once_ever"
})
create_email_campaign({
project_slug: "space-invoices",
name: "Welcome email",
template_id: "workflow-email-template-welcome",
trigger: { type: "first_seen" },
next_campaign_id: "setup-reminder-campaign-id",
frequency: "once_ever"
})HAL stores chain metadata in the target run. list_email_campaign_runs and get_email_campaign_run include triggered_by_campaign with the source workflow id, source execution id, chain id, and chain depth. Chaining has a maximum depth guard to prevent loops, and MCP rejects direct self-targeting.
frequency controls how often the campaign can run. frequency_scope controls who the limit applies to.
once_ever, once_per_session, every_time, every_30_days, every_60_days, and every_90_days.visitor, contact, company, and custom_key.custom_frequency_key can use stable values such as visitor_id, contact_id, company_id, event_id, event_name, event_value, visitor_metadata.<key>, and company_metadata.<key>.no_event_after with scope: "custom_key", use visitor_id, contact_id, or company_id. Put account-specific attributes such as plan or workspace state in predicates.Predicates let property-match campaigns target richer visitor, contact, company, and event state. Prefer discovery tools before writing property names by hand.
| Source | Typical fields |
|---|---|
visitor or custom |
Visitor metadata and custom property keys. |
contact |
Contact metadata keys linked to the visitor. |
company |
company_id, company_name, company_stage, company_created_at, company_first_seen_days_ago, company_contact_count, and company_metadata.<key>. |
event |
event_id, event_name, event_value, event_entity, company_id, and occurred_at. |
builtin |
first_seen_today, visitor_email_exists, visitor_user_id_exists, identity_status, contact_linked, company_linked, and visit_count. |
Supported operators include is_true, is_false, exists, not_exists, equals, not_equals, contains, not_contains, starts_with, and numeric comparison operators.
Normal campaign conditions are the high-level filters accepted by create_email_campaign and update_email_campaign for first-seen, event, no-event-after, and page-visit campaigns. They are evaluated as workflow condition nodes before the email step.
| Field family | Use for | Examples |
|---|---|---|
| Visitor built-ins | Known identity, URL, browser locale, visit count, and first-seen state. | visitor_email, visitor_name, visitor_user_id, page_url, browser_language, visit_count |
| Visitor metadata | Product or widget properties stored on the visitor. | visitor_metadata.locale, visitor_metadata.plan, visitor_metadata.signup_source |
| Contact metadata | Contact-level properties merged through identify or set-properties. | contact_metadata.locale, contact_metadata.lifecycle_segment |
| Company fields and metadata | Workspace/account targeting and CRM state. | company_name, company_stage, company_contact_count, company_metadata.plan |
Use browser_language when you want the locale detected from the visitor's browser. Use visitor_metadata.locale or contact_metadata.locale when your product stores an explicit user-selected locale. list_custom_properties returns condition_fields with built-ins, sampled metadata keys, and the locale metadata fields even before samples exist.
create_email_campaign({
project_slug: "space-invoices",
name: "Slovene welcome email",
template_id: "workflow-email-template-...",
trigger: { type: "event", event_name: "account.created" },
conditions: [
{ field: "visitor_metadata.locale", operator: "equals", value: "sl" },
{ field: "contact_metadata.lifecycle_segment", operator: "equals", value: "founder" }
],
frequency: "once_ever"
})Email subjects, preheaders, buttons, image alt text, and text blocks support merge variables. Built-ins include {{visitor_name}}, {{visitor_email}}, {{visitor_user_id}}, {{project_name}}, {{workflow_name}}, {{browser_language}}, and company fields such as {{company_name}}. Metadata values use the same prefixes as campaign conditions: {{visitor_metadata.locale}}, {{contact_metadata.lifecycle_segment}}, and {{company_metadata.plan}}. Bare visitor custom-property keys such as {{plan}} resolve from visitor metadata.
Add a fallback with a pipe so missing data degrades cleanly: {{visitor_name | "there"}}, {{visitor_metadata.locale | "en"}}, or {{plan | "free"}}. validate_email_template returns warnings for unknown bare variables so MCP clients can catch likely typos before enabling a campaign.
{
"subject": "Welcome {{visitor_name | \"there\"}}",
"blocks": [
{
"id": "p1",
"type": "paragraph",
"text": "Your {{company_metadata.plan | \"trial\"}} workspace is ready."
}
]
}When delay_seconds is set, HAL records a scheduled workflow execution and enqueues delayed delivery. At send time, HAL re-checks the workflow state, recipient email, and current template before sending.
scheduled, sent, failed, and skipped.reevaluate_conditions_at_send: true when the visitor, contact, or company must still match the workflow path after the delay.conditions_no_longer_match.Lifecycle emails include a signed unsubscribe footer link plus one-click unsubscribe headers. When a recipient unsubscribes, HAL stores a project-scoped suppression record and future lifecycle campaign sends to that address are skipped before provider delivery.
list_email_suppressions lists suppressed addresses for a project.suppress_email_recipient adds a manual, unsubscribe, bounce, or complaint suppression.unsuppress_email_recipient removes a suppression when an operator intentionally re-allows the address.get_email_unsubscribe_status checks whether one email is currently suppressed.status: "skipped" and a suppression reason.Use stable event_key values when sending lifecycle events from your backend or widget. Retries with the same key return a deduped response and do not re-run campaign triggers.
POST /api/v1/events/track
{
"project_id": "project_...",
"visitor_id": "visitor_...",
"event_name": "account.created",
"event_key": "account:acct_123:created",
"entity_type": "company",
"entity_id": "acct_123",
"company_id": "company_...",
"metadata": {
"plan": "trialing"
}
}Use test-send tools before enabling a campaign. Test emails are intentionally limited to users on the selected account, so an AI client cannot send arbitrary email through the project.
send_test_email_template renders a selected template with sample variables.send_test_email_campaign renders the campaign's current template and campaign context.Check that the campaign is enabled, the visitor has an email address, and the frequency scope has not already been used.
Run list_event_names. HAL returns requested and normalized event names so clients can explain what will match.
Confirm the anchor event had the same visitor, contact, or company scope as the missing event check. For company flows, include company_id on tracked events.
Inspect the campaign run. Common reasons are missing recipient email, disabled workflow, missing template, or conditions_no_longer_match.