[{"data":1,"prerenderedAt":1798},["ShallowReactive",2],{"search":3,"content-developer\u002Fautomation-internals":442,"surround-\u002Fdeveloper\u002Fautomation-internals":1793},[4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308,312,316,320,323,327,331,335,339,343,347,351,355,359,363,367,371,375,379,383,387,391,395,399,403,407,411,415,419,423,426,430,434,438],{"path":5,"title":6,"description":7},"\u002Fguide","Guide","Product guides for Owlat — a modular, self-hosted email platform. Learn how to send campaigns, run a personal mailbox, manage a team inbox, and more.",{"path":9,"title":10,"description":11},"\u002Fguide\u002Fgetting-started","Welcome to Owlat","Set up your Owlat workspace and send your first email — from deploying the stack to verifying a domain, building your audience, and launching a campaign.",{"path":13,"title":14,"description":15},"\u002Fguide\u002Fcontact-properties","Contact Properties","Custom fields that extend built-in contact data with your own values for segmentation.",{"path":17,"title":18,"description":19},"\u002Fguide\u002Ftopics","Topics","Topics are explicit audience groups you manage by hand — ideal for opt-in subscribers, imported cohorts, and organized contact buckets you target with campaigns.",{"path":21,"title":22,"description":23},"\u002Fguide\u002Fsegments","Segments","Build dynamic, rule-based contact groups from properties, email activity, and topic membership, re-evaluated from current data each time they're used.",{"path":25,"title":26,"description":27},"\u002Fguide\u002Fforms","Forms","Form Endpoints collect new contacts from your website or landing pages by exposing a public endpoint that accepts submissions and feeds them into a topic.",{"path":29,"title":30,"description":31},"\u002Fguide\u002Fcampaigns","Campaigns & Reporting","Build and send marketing campaigns to a topic or segment with the five-step wizard, optional A\u002FB testing, and full delivery reporting.",{"path":33,"title":34,"description":35},"\u002Fguide\u002Fab-testing","A\u002FB Testing","Compare two variants of a campaign on a test group, then automatically or manually send the winning version to the rest of your audience.",{"path":37,"title":38,"description":39},"\u002Fguide\u002Fautomations","Automations","Send emails automatically based on triggers, delays, and conditions — build welcome series, trial flows, and follow-ups once and let Owlat run them.",{"path":41,"title":42,"description":43},"\u002Fguide\u002Ftransactional","Transactional Emails","One-to-one emails your application triggers in response to a user action — password resets, order confirmations, welcome emails, and similar notifications.",{"path":45,"title":46,"description":47},"\u002Fguide\u002Fcreate-campaign","Create a Campaign","Walk through Owlat's five-step campaign wizard: Basics, Audience, Content, A\u002FB Test, and Review & Send.",{"path":49,"title":50,"description":51},"\u002Fguide\u002Fsend-campaign","Send & Monitor a Campaign","How to send your campaign and track its performance with real-time metrics.",{"path":53,"title":54,"description":55},"\u002Fguide\u002Fquick-start","Quick Start","The fastest path from a blank Owlat workspace to a live email campaign, from your first template through sending and reviewing results.",{"path":57,"title":58,"description":59},"\u002Fguide\u002Ftransactional-setup","Transactional Email Setup","Set up and send transactional emails like password resets and order confirmations via the Owlat API and SDKs.",{"path":61,"title":62,"description":63},"\u002Fguide\u002Fdeliverability","Deliverability","Verify sending domains, manage your blocklist, monitor sending reputation, and stay compliant so your emails reach the inbox.",{"path":65,"title":66,"description":67},"\u002Fguide\u002Fapi-keys-webhooks","API Keys & Webhooks","Create API keys for programmatic access and set up outbound webhooks to receive real-time notifications for email and contact events.",{"path":69,"title":70,"description":71},"\u002Fguide\u002Ffeature-flags","Feature flags","Owlat is modular — every feature listed in this guide can be turned on or off. This page is the user-facing overview of how to do it.",{"path":73,"title":74,"description":75},"\u002Fguide\u002Fteam-permissions","Team & Permissions","Use role-based access to control what each member of your organization can do, with Owner, Admin, and Editor roles.",{"path":77,"title":78,"description":79},"\u002Fguide\u002Faudit-logs","Audit Logs","A chronological record of significant actions in your Owlat organization, so you can see who did what and when.",{"path":81,"title":82,"description":83},"\u002Fguide\u002Fshare-links","Share Links","Create temporary preview links to share email designs with stakeholders who don't have dashboard access.",{"path":85,"title":86,"description":87},"\u002Fguide\u002Fpostbox","Postbox — Personal Email","Per-user mailboxes with a webmail interface and native IMAP\u002FSMTP support. Run your own Gmail-equivalent personal mailbox on your Owlat instance.",{"path":89,"title":90,"description":91},"\u002Fguide\u002Fmigrate-from-google","Migrate from Google","Import your full Gmail history into Owlat over IMAP, and let your AI assistant learn from every imported conversation.",{"path":93,"title":94,"description":95},"\u002Fguide\u002Fteam-inbox","Team Inbox","Triage inbound email as a team: read AI-classified threads, approve, edit or reject agent drafts, work the review queue, and manage quarantine.",{"path":97,"title":98,"description":99},"\u002Fguide\u002Femail-editor","Email Editor","A block-based visual editor for building responsive emails that render consistently across desktop, mobile, Outlook, Gmail, and Apple Mail.",{"path":101,"title":102,"description":103},"\u002Fguide\u002Fai-agent","AI Agent & Autonomy","Configure the AI agent that classifies and drafts replies to inbound mail: auto-reply settings, the health dashboard, circuit breakers, autonomy rules, and the knowledge backfill.",{"path":105,"title":106,"description":107},"\u002Fguide\u002Fknowledge-graph","Knowledge Graph","Browse, search, and manage Owlat's typed organizational knowledge — the 7 entry types, source attribution, confidence decay, relations, and how entries are extracted from mail.",{"path":109,"title":110,"description":111},"\u002Fguide\u002Ffiles","Files","Upload, browse, search, tag, and version documents in the file library.",{"path":113,"title":114,"description":115},"\u002Fguide\u002Fchat","Team Chat","Use Owlat's built-in team chat: public and private channels, direct messages, mentions, attachments, and channels linked to an inbox conversation.",{"path":117,"title":118,"description":119},"\u002Fguide\u002Fcode-tasks","Code Tasks","Queue coding-agent tasks, watch them move from queued through review, and run the code-worker sidecar that opens the pull requests.",{"path":121,"title":122,"description":123},"\u002Fguide\u002Faudience-data","Audience Data: Identities, Relationships & Timeline","Unify a contact across email, phone, and messaging channels, merge duplicates, map relationships, and read the cross-channel interaction timeline.",{"path":125,"title":126,"description":127},"\u002Fguide\u002Fimporting-contacts","Importing & Exporting Contacts","Bring contacts into Owlat from a CSV or from Mailchimp and Stripe, export them back out, and run bulk operations on your audience.",{"path":129,"title":130,"description":131},"\u002Fguide\u002Faccount","Your Account & Data","Export your data as JSON or CSV, request account deletion with a 30-day grace period, and use the onboarding checklist and the public preference center.",{"path":133,"title":134,"description":135},"\u002Fguide\u002Fchannels","Communication Channels","Configure SMS, WhatsApp, and generic-webhook channels, monitor channel health, and understand which channels are fully live today.",{"path":137,"title":138,"description":139},"\u002Fguide\u002Fdesktop-app","Desktop App","Install the Owlat desktop app, connect one or more workspaces, switch between them, and use native notifications, tray badges, shortcuts, and deep links.",{"path":141,"title":142,"description":143},"\u002Fguide\u002Femail-templates","Email Templates","Reusable email designs that define the structure, content, and personalization of every campaign and transactional message you send in Owlat.",{"path":145,"title":146,"description":147},"\u002Fguide\u002Fai-assistant","AI Assistant","Owlat's multi-turn, streaming, tool-calling AI assistant — a private chat surface that can search your workspace and draft copy, plus @assistant replies inside team chat.",{"path":149,"title":150,"description":151},"\u002Fguide\u002Fsecurity-scanning","Sending Security & Scanning","Owlat's security scanning: a content check for spam and phishing, an attachment scan for malware, and a Google Safe Browsing URL check. Suspicious content goes to a review queue.",{"path":153,"title":154,"description":155},"\u002Fguide\u002Fsystem-updates","System & Updates","The owner-only System & Updates screen: your current Owlat version, container health, LLM spend, and the in-app one-click updater with history.",{"path":157,"title":158,"description":159},"\u002Fguide\u002Foperating-modes","Operating Modes","The different ways to run Owlat at a company — read external mailboxes over IMAP, send transactional or marketing email through a delivery provider, host your own mail server, or run a team inbox with AI — and the rules that keep each combination coherent.",{"path":161,"title":162,"description":163},"\u002Fguide\u002Fsaved-blocks","Saved Blocks","Create reusable, linked content blocks you can drop into any email — edit one and every email that uses it updates automatically.",{"path":165,"title":166,"description":167},"\u002Fguide\u002Fmedia-library","Media Library","Manage, organize, search, and reuse images and files across your emails from one centralized hub.",{"path":169,"title":170,"description":171},"\u002Fguide\u002Femail-theme","Email Theme","Set your organization's default colors, font, and email width so every new template starts from a consistent baseline.",{"path":173,"title":174,"description":175},"\u002Fguide\u002Ftranslations","Translations","Send one email in multiple languages: add per-language translations to a single template and Owlat picks the right version for each recipient.",{"path":177,"title":178,"description":179},"\u002Fguide\u002Fcontacts","Contacts","How to add, view, organize, and manage contacts in Owlat, including sources, the contact detail tabs, and subscription compliance.",{"path":181,"title":182,"description":183},"\u002Fapi","API Overview","Owlat exposes authenticated API endpoints under your Convex site URL.",{"path":185,"title":186,"description":187},"\u002Fapi\u002Fwebhooks","Webhooks","Owlat supports both outbound customer webhooks and inbound provider webhooks.",{"path":189,"title":190,"description":191},"\u002Fapi\u002Fpublic-endpoints","Public Endpoints","These routes are public-facing and usually accessed from email links or embedded forms.",{"path":193,"title":194,"description":195},"\u002Fapi\u002Fwebhook-payloads","Webhook Payloads","The authoritative wire contract for outbound webhooks: envelope, signature headers, per-event data shapes, and payload versioning.",{"path":197,"title":198,"description":199},"\u002Fapi\u002Finbound-channels","Inbound Channel Webhooks","Provider webhook reference for inbound SMS, WhatsApp, and generic-channel messages, plus the MTA mailbox and credential callbacks.",{"path":201,"title":202,"description":203},"\u002Fapi\u002Fauthentication","Authentication","Secure API access with organization-scoped API keys.",{"path":205,"title":206,"description":207},"\u002Fapi\u002Fsdk","TypeScript SDK","Typed client for the Owlat API, usable from Node.js, Bun, Deno, or any server-side JavaScript runtime.",{"path":209,"title":210,"description":211},"\u002Fapi\u002Fsdk-java","Java SDK","The official `owlat-sdk` package provides a typed client for interacting with the Owlat API from any JVM application. Requires Java 11+.",{"path":213,"title":214,"description":215},"\u002Fapi\u002Fcontacts","Contacts API","Manage contacts for your organization.",{"path":217,"title":218,"description":219},"\u002Fapi\u002Ftopics","Topics API","Manage topic membership through authenticated endpoints.",{"path":221,"title":222,"description":223},"\u002Fapi\u002Fevents","Events API","Send contact events to drive segmentation and automation triggers.",{"path":225,"title":226,"description":227},"\u002Fapi\u002Ftransactional","Transactional API","Send published transactional templates to a recipient.",{"path":229,"title":230,"description":231},"\u002Fapi\u002Fforms","Forms API","Capture subscribers through public form endpoints.",{"path":233,"title":234,"description":235},"\u002Fdeveloper","Developer Guide","Technical architecture, feature-flag model, and provider abstractions used by Owlat.",{"path":237,"title":238,"description":239},"\u002Fdeveloper\u002Fmta-system","MTA System","Owlat's custom Mail Transfer Agent for direct SMTP delivery with intelligent rate limiting, bounce processing, and IP warming.",{"path":241,"title":242,"description":243},"\u002Fdeveloper\u002Ffeature-flags","Feature flags — developer reference","How the Owlat feature flag system works: single source of truth, dependency resolution, docker profile mapping, and how to add a new flag.",{"path":245,"title":246,"description":247},"\u002Fdeveloper\u002Fhow-email-works","How Email Works","A technical deep-dive into how email actually works — from SMTP and DNS to authentication, deliverability, and the differences between marketing and private email.",{"path":249,"title":250,"description":251},"\u002Fdeveloper\u002Femail-security","Email Security","Content scanning, attachment validation, URL reputation checking, and malware detection for outbound emails.",{"path":253,"title":254,"description":255},"\u002Fdeveloper\u002Fpostbox-architecture","Postbox Architecture","How the Postbox personal-mail feature is wired — schema, IMAP server, app-password auth, outbound relay, inbound delivery, and external mailboxes.",{"path":257,"title":258,"description":259},"\u002Fdeveloper\u002Fproviders","Providers","Pluggable provider abstractions for LLM, email sending, notifications, vector stores, and analytics, selected per-deployment so self-hosters can swap implementations without code changes.",{"path":261,"title":262,"description":263},"\u002Fdeveloper\u002Fcampaign-internals","Campaign Internals","How the campaign backend works: two status machines, send pre-flight, the send orchestrator, emailSends records, and the priority workpools.",{"path":265,"title":266,"description":267},"\u002Fdeveloper\u002Faudience-internals","Audience Internals","Backend reference for contact resolution, the double opt-in lifecycle, topic subscription, the conditions registry, and segment evaluation.",{"path":269,"title":270,"description":271},"\u002Fdeveloper\u002Fautomation-internals","Automation Internals","How the automation run engine works: the step walker, the lifecycle state machine, trigger fanout, the three step types, and the resilience cron.",{"path":273,"title":274,"description":275},"\u002Fdeveloper\u002Fdeliverability-infrastructure","Deliverability Infrastructure","The Convex-side deliverability backend: provider routing, health-aware failover, sending reputation with auto-enforcement, IP warming cache, the blocklist, and the content-scan gate.",{"path":277,"title":278,"description":279},"\u002Fdeveloper\u002Farchitecture","Architecture Overview","Owlat follows a modern serverless architecture with real-time capabilities.",{"path":281,"title":282,"description":283},"\u002Fdeveloper\u002Fplatform-operations","Platform Operations","Operator reference for abuse status and the sending gate, the platform-admin roster, content review, org deletion, in-app self-update, dev endpoints, crons, and migrations.",{"path":285,"title":286,"description":287},"\u002Fdeveloper\u002Fscopes","Scopes","What each app and package in the Owlat monorepo is responsible for.",{"path":289,"title":290,"description":291},"\u002Fdeveloper\u002Fself-hosting","Self-Hosting","Deploy Owlat on your own infrastructure with Docker Compose. Complete guide from first boot to production.",{"path":293,"title":294,"description":295},"\u002Fdeveloper\u002Fself-hosting-config","Self-Hosting Configuration","Complete reference for Docker environment variables, Convex backend variables, service topology, and volume persistence.",{"path":297,"title":298,"description":299},"\u002Fdeveloper\u002Fself-hosting-dns-email","DNS & Email Setup","Configure DNS records, DKIM signing, SPF, DMARC, and bounce handling for reliable email delivery.",{"path":301,"title":302,"description":303},"\u002Fdeveloper\u002Fself-hosting-production","Production Deployment","Secure your self-hosted Owlat instance with TLS, firewall rules, backups, and monitoring.",{"path":305,"title":306,"description":307},"\u002Fdeveloper\u002Fself-hosting-maintenance","Maintenance & Updates","Keep your self-hosted Owlat instance up to date, manage backups, scale performance, and troubleshoot common issues.",{"path":309,"title":310,"description":311},"\u002Fdeveloper\u002Fself-hosting-desktop","Desktop Installer","Install Owlat on a bare Linux VPS straight from the desktop app over SSH — no terminal — with a live, animated provisioning timeline.",{"path":313,"title":314,"description":315},"\u002Fdeveloper\u002Fsetup-cli","Setup CLI & Installer","Operator reference for the Owlat self-host tooling: the install.sh one-liner, the owlat-setup CLI, the convex-deploy flow, and admin bootstrap.",{"path":317,"title":318,"description":319},"\u002Fdeveloper\u002Fconvex","Convex Backend","Owlat uses Convex as its serverless backend, providing real-time subscriptions, ACID transactions, and TypeScript-first development.",{"path":321,"title":202,"description":322},"\u002Fdeveloper\u002Fauthentication","Owlat uses BetterAuth with the Convex adapter for authentication and organization (team) management.",{"path":324,"title":325,"description":326},"\u002Fdeveloper\u002Femail-system","Email System","Owlat's email system consists of a visual editor, template management, and multi-provider sending infrastructure.",{"path":328,"title":329,"description":330},"\u002Fdeveloper\u002Femail-renderer","Email Renderer","The @owlat\u002Femail-renderer package converts editor JSON blocks into production-ready HTML emails with cross-client compatibility, CSS inlining, dark mode, and Outlook VML fallbacks.",{"path":332,"title":333,"description":334},"\u002Fdeveloper\u002Fenvironment-variables","Environment Variables","Reference for every environment variable Owlat reads across the Convex backend, web app, MTA, IMAP server, and mail-sync worker.",{"path":336,"title":337,"description":338},"\u002Fdeveloper\u002Fcomponents","Component Library","Reference for the reusable, auto-imported Vue UI components shipped in the packages\u002Fui layer.",{"path":340,"title":341,"description":342},"\u002Fdeveloper\u002Fdecisions","Architectural Decision Records","The architectural decision records for the Owlat project, each capturing the context, the decision, and the trade-offs involved.",{"path":344,"title":345,"description":346},"\u002Fdeveloper\u002Fdecisions\u002F009-model-routing","ADR-009: Task-Based Model Routing","Why Owlat supports per-task LLM model selection instead of using a single model for all pipeline steps.",{"path":348,"title":349,"description":350},"\u002Fdeveloper\u002Fdecisions\u002F010-listing-engine","ADR-010: Listing Engine","Why Owlat replaced four incompatible list-query contracts with one generic listing engine driven by per-entity descriptors.",{"path":352,"title":353,"description":354},"\u002Fdeveloper\u002Fdecisions\u002F001-custom-email-renderer","ADR-001: Custom Email Renderer Over MJML","Why Owlat built a custom table-based HTML email renderer instead of using MJML, gaining full control over VML, dark mode, and per-client rendering.",{"path":356,"title":357,"description":358},"\u002Fdeveloper\u002Fdecisions\u002F002-convex-backend","ADR-002: Convex as Backend","Why Owlat chose Convex over PostgreSQL and Firebase for real-time reactivity, co-located TypeScript logic, and zero-config scaling.",{"path":360,"title":361,"description":362},"\u002Fdeveloper\u002Fdecisions\u002F003-notion-like-builder","ADR-003: Notion-like Email Builder","Why Owlat replaced the traditional 3-panel email editor with a Notion-like single-column canvas for inline WYSIWYG editing.",{"path":364,"title":365,"description":366},"\u002Fdeveloper\u002Fdecisions\u002F004-monorepo-bun-workspaces","ADR-004: Monorepo with Bun Workspaces","Why Owlat uses a monorepo with Bun workspaces and Turborepo for fast installs, atomic cross-package changes, and cached CI.",{"path":368,"title":369,"description":370},"\u002Fdeveloper\u002Fdecisions\u002F005-custom-mta","ADR-005: Custom MTA","Why Owlat built a custom Mail Transfer Agent instead of relying solely on third-party email providers.",{"path":372,"title":373,"description":374},"\u002Fdeveloper\u002Fdecisions\u002F006-self-hosted-convex","ADR-006: Self-Hosted Convex","Why Owlat uses the open-source Convex backend for self-hosting instead of migrating to a different database.",{"path":376,"title":377,"description":378},"\u002Fdeveloper\u002Fdecisions\u002F007-pluggable-llm","ADR-007: Pluggable LLM Provider","Why Owlat uses the Vercel AI SDK with a provider abstraction layer instead of hardcoding a single LLM vendor.",{"path":380,"title":381,"description":382},"\u002Fdeveloper\u002Fdecisions\u002F008-process-architecture","ADR-008: Agent Process Architecture","Why Owlat processes inbound messages with a self-scheduling step walker plus a lifecycle coordinator instead of one sequential function.",{"path":384,"title":385,"description":386},"\u002Fexamples","Examples","Copy-pasteable integration patterns for common Owlat use cases.",{"path":388,"title":389,"description":390},"\u002Fexamples\u002Fwelcome-email","Welcome Email","Send a personalized welcome email when a new user signs up.",{"path":392,"title":393,"description":394},"\u002Fexamples\u002Fbilling-email","Billing Email","Send a billing receipt with an invoice PDF attached after a successful payment.",{"path":396,"title":397,"description":398},"\u002Fexamples\u002Fevent-automation","Event Automation","Trigger automations with custom events for trial lifecycle, feature adoption, and more.",{"path":400,"title":401,"description":402},"\u002Fexamples\u002Fcontact-sync","Contact Sync","Sync contacts from your database to Owlat using upsert patterns and bulk operations.",{"path":404,"title":405,"description":406},"\u002Fexamples\u002Fwebhook-handler","Webhook Handler","Handle Owlat delivery webhooks with signature verification and event routing.",{"path":408,"title":409,"description":410},"\u002Fexamples\u002Fmultilingual-email","Multilingual Email","Send emails in the recipient's preferred language using template translations.",{"path":412,"title":413,"description":414},"\u002Fvision","Vision","Where Owlat is heading — from email platform to unified communication intelligence powered by AI agents.",{"path":416,"title":417,"description":418},"\u002Fvision\u002Fself-hosting","Self-Hosting Architecture","How Owlat runs as a fully self-hosted stack using Docker Compose — open-source Convex backend, custom MTA, and a pluggable LLM provider.",{"path":420,"title":421,"description":422},"\u002Fvision\u002Fagent-pipeline","Agent Pipeline","Technical architecture for the inbound email agent pipeline — step modules, the walker, security scanning, threading, and human review.",{"path":424,"title":106,"description":425},"\u002Fvision\u002Fknowledge-graph","Technical architecture for Owlat's typed knowledge storage — how organizational knowledge is stored, searched, decayed, and maintained.",{"path":427,"title":428,"description":429},"\u002Fvision\u002Fmulti-channel","Multi-Channel & CRM","Technical architecture for channel adapters, unified messaging, contact identity unification, and the CRM hub.",{"path":431,"title":432,"description":433},"\u002Fvision\u002Ffile-system","Semantic File System","Technical architecture for Owlat's semantic file storage — version tracking with provenance today, plus the planned embedding-based retrieval and auto-tagging layer.",{"path":435,"title":436,"description":437},"\u002Fvision\u002Fdesktop-app","Desktop App & Advanced Agents","Architecture of the Owlat desktop shell, visualization agent, adaptive dashboard, agent health, graduated autonomy, and coding agents.",{"path":439,"title":440,"description":441},"\u002Fvision\u002Froadmap","Roadmap","What's planned next for Owlat — the documented-but-unbuilt pieces still being wired, and the enhancements on our radar.",{"id":443,"title":270,"body":444,"description":271,"extension":1787,"meta":1788,"navigation":1789,"path":269,"seo":1790,"stem":1791,"__hash__":1792},"content\u002F3.developer\u002F18.automation-internals.md",{"type":445,"value":446,"toc":1761},"minimark",[447,460,500,505,516,550,555,574,601,605,612,670,704,708,727,731,743,751,755,784,794,812,847,851,932,951,955,965,1050,1066,1086,1089,1096,1172,1179,1273,1300,1320,1324,1348,1352,1390,1400,1417,1421,1448,1471,1475,1491,1518,1522,1532,1590,1620,1624,1632,1679,1707,1743,1747,1751,1754,1757],[448,449,450,451,454,455,459],"p",{},"This page is the developer reference for how trigger-based automations execute under the hood. For the product-level walkthrough of building automations in the UI, see ",[452,453,38],"a",{"href":37},". Everything here lives in ",[456,457,458],"code",{},"apps\u002Fapi\u002Fconvex\u002Fautomations\u002F",".",[448,461,462,463,466,467,470,471,474,475,478,479,482,483,487,488,491,492,495,496,499],{},"An automation is one row in the ",[456,464,465],{},"automations"," table: a ",[456,468,469],{},"triggerType",", an optional ",[456,472,473],{},"triggerConfig",", a ",[456,476,477],{},"status",", and an ordered list of ",[456,480,481],{},"automationSteps",". When a trigger fires for a contact, the ",[484,485,486],"strong",{},"Trigger fanout"," inserts an ",[456,489,490],{},"automationRuns"," row and schedules the ",[484,493,494],{},"Step walker",", which executes one step at a time, recording each as an ",[456,497,498],{},"automationStepRuns"," row.",[501,502,504],"h2",{"id":503},"the-run-engine-step-walker","The run engine (step walker)",[448,506,507,508,511,512,515],{},"The walker is ",[456,509,510],{},"apps\u002Fapi\u002Fconvex\u002Fautomations\u002FstepWalker.ts"," — a ",[456,513,514],{},"'use node'"," module that owns the actual execution of an automation. It is the single entry point for running steps; per-step logic lives in step modules it dispatches to.",[448,517,518,519,522,523,526,527,530,531,534,535,538,539,542,543,546,547,459],{},"A run advances one step per ",[456,520,521],{},"executeStep"," invocation. There is no ",[456,524,525],{},"if (step.kind === ...)"," branching in the walker — it looks up the step's module via ",[456,528,529],{},"stepModuleFor(step.stepType)"," and calls ",[456,532,533],{},"parseConfig"," then ",[456,536,537],{},"execute",". The module returns a ",[456,540,541],{},"StepOutcome"," of ",[456,544,545],{},"{ status: 'completed', emailSendId?, nextStepIndex? }"," or ",[456,548,549],{},"{ status: 'failed', error }",[551,552,554],"h3",{"id":553},"atomic-step-claim","Atomic step claim",[448,556,557,558,561,562,565,566,569,570,573],{},"Two independent schedulers can target the same pending step: the original ",[456,559,560],{},"ctx.scheduler.runAfter(...)"," from when the step was scheduled, ",[484,563,564],{},"and"," the ",[456,567,568],{},"process pending delays"," cron (which re-dispatches any pending step whose delay has elapsed). To stop a delay-gated email going out twice, a fresh dispatch (",[456,571,572],{},"retryCount === 0",") must atomically claim the step before executing.",[448,575,576,577,580,581,584,585,588,589,592,593,596,597,600],{},"The claim is ",[456,578,579],{},"markStepExecuting"," in ",[456,582,583],{},"apps\u002Fapi\u002Fconvex\u002Fautomations\u002FstepExecutorQueries.ts"," — a single ",[456,586,587],{},"pending → executing"," compare-and-set. Only the first caller wins; a second invocation sees ",[456,590,591],{},"executing"," (or a later status), gets ",[456,594,595],{},"{ claimed: false }",", and silently drops. Retries (",[456,598,599],{},"retryCount > 0",") re-enter on a step run this same chain already owns, so they skip the claim.",[551,602,604],{"id":603},"retries","Retries",[448,606,607,608,611],{},"A step whose module returns ",[456,609,610],{},"failed"," (or throws) is retried with a fixed backoff schedule before the run is abandoned:",[613,614,615,628],"table",{},[616,617,618],"thead",{},[619,620,621,625],"tr",{},[622,623,624],"th",{},"Attempt",[622,626,627],{},"Delay before retry",[629,630,631,640,648,656],"tbody",{},[619,632,633,637],{},[634,635,636],"td",{},"1st retry",[634,638,639],{},"1s",[619,641,642,645],{},[634,643,644],{},"2nd retry",[634,646,647],{},"5s",[619,649,650,653],{},[634,651,652],{},"3rd retry",[634,654,655],{},"30s",[619,657,658,661],{},[634,659,660],{},"After 3 retries",[634,662,663,664,666,667],{},"step marked ",[456,665,610],{},", run ",[456,668,669],{},"cancelled",[448,671,672,675,676,679,680,683,684,687,688,691,692,695,696,699,700,703],{},[456,673,674],{},"MAX_RETRY_ATTEMPTS"," is 3 and ",[456,677,678],{},"RETRY_DELAYS_MS"," is ",[456,681,682],{},"[1000, 5000, 30000]"," (ms), both in ",[456,685,686],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002Fconstants.ts",". On the final failure the walker calls ",[456,689,690],{},"markStepFailed"," and then ",[456,693,694],{},"cancelAutomationRun"," — a step that exhausts its retries cancels the whole run. The walker also calls ",[456,697,698],{},"internal.automations.lifecycle.recordRunFailure","; after 5 consecutive run failures (",[456,701,702],{},"AUTOMATION_FAILURE_BREAKER_THRESHOLD",") the automation is auto-paused via the circuit breaker.",[551,705,707],{"id":706},"loop-cap","Loop cap",[448,709,710,711,714,715,718,719,722,723,726],{},"A condition step can branch to ",[484,712,713],{},"any"," target index, including an earlier step (the editor allows it). Without a guard, a backward branch would loop forever and re-send the email step on every pass. The walker enforces ",[456,716,717],{},"MAX_STEPS_PER_RUN = 100",": each successful claim bumps ",[456,720,721],{},"automationRuns.stepsExecuted",", and when that count exceeds the cap the run is marked failed and cancelled with the message ",[456,724,725],{},"Automation exceeded 100 step executions — cancelled to prevent a loop",". 100 is far above any legitimate linear automation length.",[551,728,730],{"id":729},"active-status-guard","Active-status guard",[448,732,733,735,736,739,740,459],{},[456,734,521],{}," re-reads the parent automation on every step. If ",[456,737,738],{},"automation.status !== 'active'",", it marks the current step failed and ",[484,741,742],{},"cancels the run",[744,745,748],"callout",{"title":746,"type":747},"Pause cancels due runs","warning",[448,749,750],{},"Pausing (or reverting) an automation does not retroactively touch in-flight runs, but the next step that comes due for any running run will find the automation non-active and cancel that run. In practice, pausing an automation drains its running cohort as each contact's next step fires — it is not a freeze-and-resume.",[501,752,754],{"id":753},"automation-lifecycle-state-machine","Automation lifecycle state machine",[448,756,757,758,761,762,765,766,542,769,771,772,775,776,779,780,783],{},"The ",[456,759,760],{},"automations.status"," machine lives in ",[456,763,764],{},"apps\u002Fapi\u002Fconvex\u002Fautomations\u002Flifecycle.ts"," — the ",[484,767,768],{},"single writer",[456,770,477],{}," and its companion fields ",[456,773,774],{},"activatedAt",", ",[456,777,778],{},"pausedAt",", and ",[456,781,782],{},"updatedAt"," (ADR-0024). Three states, four legal edges:",[785,786,791],"pre",{"className":787,"code":789,"language":790},[788],"language-text","draft   → active            (activate; validates trigger config + ≥1 step)\nactive  → paused            (pause)\npaused  → active            (resume; re-validates trigger config + ≥1 step)\npaused  → draft             (revertToDraft)\n","text",[456,792,789],{"__ignoreMap":793},"",[448,795,796,799,800,803,804,807,808,811],{},[456,797,798],{},"active → draft"," is refused as ",[456,801,802],{},"illegal_edge"," — admins must pause first. The legal-edge graph is a single ",[456,805,806],{},"LEGAL_EDGES"," constant rather than scattered ",[456,809,810],{},"if (status !== ...)"," checks.",[448,813,814,815,775,818,775,821,775,824,580,827,830,831,834,835,838,839,842,843,846],{},"The public mutations (",[456,816,817],{},"activate",[456,819,820],{},"pause",[456,822,823],{},"resume",[456,825,826],{},"revertToDraft",[456,828,829],{},"automations\u002Fautomations.ts",") are thin auth shells: they check the ",[456,832,833],{},"automations:manage"," permission and dispatch to ",[456,836,837],{},"internal.automations.lifecycle.transition",", which returns a typed outcome rather than throwing. The shell's ",[456,840,841],{},"reasonToMessage"," maps the typed ",[456,844,845],{},"reason"," to a user-facing string.",[551,848,850],{"id":849},"transition-outcomes","Transition outcomes",[613,852,853,872],{},[616,854,855],{},[619,856,857,866,869],{},[622,858,859,861,862,865],{},[456,860,845],{}," (on ",[456,863,864],{},"ok: false",")",[622,867,868],{},"Meaning",[622,870,871],{},"Shell message",[629,873,874,887,901,917],{},[619,875,876,881,884],{},[634,877,878],{},[456,879,880],{},"automation_not_found",[634,882,883],{},"Row missing",[634,885,886],{},"\"Automation not found\"",[619,888,889,893,898],{},[634,890,891],{},[456,892,802],{},[634,894,895,896],{},"Edge not in ",[456,897,806],{},[634,899,900],{},"\"Automation is not in a state that allows this transition\"",[619,902,903,908,914],{},[634,904,905],{},[456,906,907],{},"no_steps",[634,909,910,913],{},[456,911,912],{},"→ active"," with zero steps",[634,915,916],{},"\"Automation must have at least one step to be activated\"",[619,918,919,924,929],{},[634,920,921],{},[456,922,923],{},"invalid_trigger_config",[634,925,926,928],{},[456,927,912],{}," with missing trigger config",[634,930,931],{},"\"Automation trigger is missing required configuration\"",[448,933,934,935,938,939,942,943,946,947,950],{},"A duplicate same-state attempt (",[456,936,937],{},"from === to",") is idempotent: it returns ",[456,940,941],{},"{ ok: true, applied: 'recorded' }",", writes an audit-log row marked ",[456,944,945],{},"no_op: true",", and emits ",[484,948,949],{},"no"," PostHog event and no patch.",[551,952,954],{"id":953},"effects-per-transition","Effects per transition",[448,956,957,958,960,961,964],{},"Every real transition writes an audit-log row ",[484,959,564],{}," a PostHog ",[456,962,963],{},"track_event","; self-loops write only the audit row.",[613,966,967,980],{},[616,968,969],{},[619,970,971,974,977],{},[622,972,973],{},"Transition",[622,975,976],{},"Audit action",[622,978,979],{},"PostHog event",[629,981,982,999,1016,1033],{},[619,983,984,989,994],{},[634,985,986],{},[456,987,988],{},"draft → active",[634,990,991],{},[456,992,993],{},"automation.activated",[634,995,996],{},[456,997,998],{},"automation_activated",[619,1000,1001,1006,1011],{},[634,1002,1003],{},[456,1004,1005],{},"active → paused",[634,1007,1008],{},[456,1009,1010],{},"automation.paused",[634,1012,1013],{},[456,1014,1015],{},"automation_paused",[619,1017,1018,1023,1028],{},[634,1019,1020],{},[456,1021,1022],{},"paused → active",[634,1024,1025],{},[456,1026,1027],{},"automation.resumed",[634,1029,1030],{},[456,1031,1032],{},"automation_resumed",[619,1034,1035,1040,1045],{},[634,1036,1037],{},[456,1038,1039],{},"paused → draft",[634,1041,1042],{},[456,1043,1044],{},"automation.reverted_to_draft",[634,1046,1047],{},[456,1048,1049],{},"automation_reverted_to_draft",[448,1051,1052,1054,1055,1057,1058,1060,1061,1060,1063,1065],{},[456,1053,912],{}," runs two preconditions before patching: at least one ",[456,1056,481],{}," row, and a valid trigger config for the trigger type. Both run on ",[456,1059,988],{}," ",[484,1062,564],{},[456,1064,1022],{},", so a resume cannot silently re-enter a broken active state.",[448,1067,1068,1069,775,1072,775,1075,1078,1079,1082,1083,459],{},"Lifetime stats counters (",[456,1070,1071],{},"statsEntered",[456,1073,1074],{},"statsActive",[456,1076,1077],{},"statsCompleted",") are ",[484,1080,1081],{},"not"," touched by the lifecycle — they are owned by the trigger fanout and by run completion\u002Fcancellation in ",[456,1084,1085],{},"stepExecutorQueries.ts",[501,1087,486],{"id":1088},"trigger-fanout",[448,1090,1091,1092,1095],{},"Triggers are wired through per-kind modules registered in ",[456,1093,1094],{},"apps\u002Fapi\u002Fconvex\u002Fautomations\u002Ftriggers.ts",". Four trigger kinds ship:",[613,1097,1098,1112],{},[616,1099,1100],{},[619,1101,1102,1105,1108],{},[622,1103,1104],{},"Trigger kind",[622,1106,1107],{},"Fires when",[622,1109,1110],{},[456,1111,473],{},[629,1113,1114,1127,1142,1157],{},[619,1115,1116,1121,1124],{},[634,1117,1118],{},[456,1119,1120],{},"contact_created",[634,1122,1123],{},"A contact is created",[634,1125,1126],{},"none (always matches)",[619,1128,1129,1134,1137],{},[634,1130,1131],{},[456,1132,1133],{},"contact_updated",[634,1135,1136],{},"A watched property changes",[634,1138,1139],{},[456,1140,1141],{},"{ propertyKey }",[619,1143,1144,1149,1152],{},[634,1145,1146],{},[456,1147,1148],{},"event_received",[634,1150,1151],{},"A named event is sent for a contact",[634,1153,1154],{},[456,1155,1156],{},"{ eventName }",[619,1158,1159,1164,1167],{},[634,1160,1161],{},[456,1162,1163],{},"topic_subscribed",[634,1165,1166],{},"A contact subscribes to a topic",[634,1168,1169],{},[456,1170,1171],{},"{ topicId }",[448,1173,1174,1175,1178],{},"The shared ",[456,1176,1177],{},"fireTrigger"," walker runs the fanout pipeline for one kind:",[1180,1181,1182,1186,1200,1204,1220,1224,1239,1243],"steps",{},[551,1183,1185],{"id":1184},"fetch-matching-automations","Fetch matching automations",[448,1187,1188,1189,1192,1193,775,1196,1199],{},"Query active automations with this trigger via the ",[456,1190,1191],{},"by_status_trigger"," index (",[456,1194,1195],{},"status = 'active'",[456,1197,1198],{},"triggerType = kind",").",[551,1201,1203],{"id":1202},"evaluate-each","Evaluate each",[448,1205,1206,1207,1209,1210,1212,1213,1216,1217,1219],{},"Per automation: narrow ",[456,1208,473],{}," through the module's ",[456,1211,533],{},", then evaluate ",[456,1214,1215],{},"module.matches(input, config)",". ",[456,1218,1120],{}," always matches; the other three compare the fired input against the persisted config.",[551,1221,1223],{"id":1222},"skip-duplicates-and-empties","Skip duplicates and empties",[448,1225,1226,1227,1230,1231,1234,1235,1238],{},"Skip the automation if the contact already has a ",[456,1228,1229],{},"running"," run for it (the ",[456,1232,1233],{},"by_automation_and_contact"," index filtered to ",[456,1236,1237],{},"status: 'running'","), and skip if the automation has no steps.",[551,1240,1242],{"id":1241},"insert-and-schedule","Insert and schedule",[448,1244,1245,1246,1248,1249,1252,1253,1255,1256,1258,1259,1261,1262,1265,1266,1269,1270,459],{},"Insert an ",[456,1247,490],{}," row at ",[456,1250,1251],{},"currentStepIndex: 0"," with ",[456,1254,1237],{},", bump the ",[456,1257,1071],{}," shard (",[456,1260,1074],{}," is derived as ",[456,1263,1264],{},"entered − completed − cancelled"," by the rollup, not incremented), attach any ",[456,1267,1268],{},"triggerData"," the module built, and schedule ",[456,1271,1272],{},"internal.automations.stepWalker.startAutomationRun",[448,1274,1275,1276,775,1279,775,1282,775,1285,1288,1289,1292,1293,1296,1297,1299],{},"The per-kind wrapper mutations (",[456,1277,1278],{},"fireContactCreatedTrigger",[456,1280,1281],{},"fireContactUpdatedTrigger",[456,1283,1284],{},"fireEventReceivedTrigger",[456,1286,1287],{},"fireTopicSubscribedTrigger",") are internal — they are called from the contact, topic, and event code paths. Events specifically come in through ",[456,1290,1291],{},"sendEvent"," (also internal), whose only public entry point is the API-key-authenticated ",[456,1294,1295],{},"POST \u002Fapi\u002Fv1\u002Fevents"," route; it is deliberately ",[484,1298,1081],{}," on the public Convex client API so an anonymous caller cannot fabricate events.",[744,1301,1304],{"title":1302,"type":1303},"Run start vs. step claim","info",[448,1305,1306,1309,1310,1312,1313,1316,1317,459],{},[456,1307,1308],{},"startAutomationRun"," schedules step 0 (honoring its entry delay) and creates the first ",[456,1311,498],{}," row. From there, each completed step calls ",[456,1314,1315],{},"advanceToStep",", which schedules the next step or, when the index runs past the last step, marks the run ",[456,1318,1319],{},"completed",[501,1321,1323],{"id":1322},"step-types","Step types",[448,1325,1326,1327,1330,1331,1334,1335,775,1337,1339,1340,1343,1344,1347],{},"Three step kinds ship, registered in ",[456,1328,1329],{},"apps\u002Fapi\u002Fconvex\u002Fautomations\u002Fsteps.ts"," and each living under ",[456,1332,1333],{},"steps\u002F\u003Ckind>\u002F",". The walker dispatches to them uniformly; only their ",[456,1336,533],{},[456,1338,537],{},", and optional ",[456,1341,1342],{},"entryDelay","\u002F",[456,1345,1346],{},"enrichForQuery"," differ.",[551,1349,1351],{"id":1350},"email","Email",[448,1353,1354,1357,1358,1361,1362,1365,1366,1369,1370,1373,1374,1377,1378,1381,1382,1384,1385,1389],{},[456,1355,1356],{},"steps\u002Femail\u002Findex.ts",". Config is ",[456,1359,1360],{},"{ emailTemplateId, subjectOverride? }",". On execute it loads the template, resolves the org's default sender, composes the subject and body for the ",[456,1363,1364],{},"automation"," send kind (no tracking, no footer), and ",[484,1367,1368],{},"enqueues"," a ",[456,1371,1372],{},"transactionalSends"," row onto the transactional pool via ",[456,1375,1376],{},"internal.delivery.enqueue.enqueueNonCampaignSend"," (returning the enqueued send id as ",[456,1379,1380],{},"emailSendId","). Provider resolution, dispatch, and the Send lifecycle transition all happen asynchronously on the worker — ",[456,1383,1319],{}," here means the send was ",[1386,1387,1388],"em",{},"enqueued",", not delivered.",[744,1391,1393],{"title":1392,"type":1303},"Email steps need an email address",[448,1394,1395,1396,1399],{},"Contacts that arrived via phone\u002FSMS\u002FWhatsApp\u002Fgeneric channels have no email. The email step fails explicitly with ",[456,1397,1398],{},"Contact has no email address"," for those contacts, so the run log records why the dispatch was skipped.",[448,1401,1402,1403,1405,1406,1409,1410,1412,1413,1416],{},"The email step is the only step that returns an ",[456,1404,1380],{}," (the id of the enqueued Send row, not a provider message ID), which the walker stores on the step run via ",[456,1407,1408],{},"markStepCompleted",". The step also implements ",[456,1411,1346],{}," so ",[456,1414,1415],{},"getWithRelations"," can join the referenced template for the editor.",[551,1418,1420],{"id":1419},"delay","Delay",[448,1422,1423,1357,1426,1429,1430,679,1433,1436,1437,1439,1440,1443,1444,1447],{},[456,1424,1425],{},"steps\u002Fdelay\u002Findex.ts",[456,1427,1428],{},"{ duration, unit }"," where ",[456,1431,1432],{},"unit",[456,1434,1435],{},"minutes | hours | days | weeks",". The delay step is the only kind that implements ",[456,1438,1342],{},": ",[456,1441,1442],{},"computeEntryDelay"," converts the config to milliseconds via ",[456,1445,1446],{},"delayConfigToMs",", and the walker uses that as the look-ahead delay when scheduling the step.",[448,1449,1450,1451,1454,1455,1457,1458,1460,1461,1463,1464,1467,1468,1470],{},"The delay therefore happens ",[484,1452,1453],{},"before"," the step is dispatched, not during execution — by the time the delay step's ",[456,1456,537],{}," runs, the wait has already elapsed, so ",[456,1459,537],{}," is a no-op that immediately returns ",[456,1462,1319],{},". The pending step's ",[456,1465,1466],{},"delayUntil"," is stamped on the ",[456,1469,498],{}," row so the cron can recover it.",[551,1472,1474],{"id":1473},"condition","Condition",[448,1476,1477,1357,1480,1483,1484,1486,1487,1490],{},[456,1478,1479],{},"steps\u002Fcondition\u002Findex.ts",[456,1481,1482],{},"{ condition, yesBranchStepIndex, noBranchStepIndex }",". On execute it serializes the canonical ",[456,1485,1474],{}," and evaluates it against the contact via ",[456,1488,1489],{},"internal.automations.steps.condition.queries.evaluateConditionForContact"," (an action can't read the DB directly, so evaluation runs in a query).",[448,1492,1493,1494,1497,1498,1501,1502,1505,1506,1509,1510,1513,1514,1517],{},"A truthy result branches to ",[456,1495,1496],{},"yesBranchStepIndex",", a falsy result to ",[456,1499,1500],{},"noBranchStepIndex",". The chosen index is returned as the outcome's ",[456,1503,1504],{},"nextStepIndex",", overriding the default sequential ",[456,1507,1508],{},"currentStepIndex + 1",". A ",[456,1511,1512],{},"null"," branch target means \"fall through to the next sequential step.\" Because a branch can point backward, the condition step is exactly why the walker enforces the ",[456,1515,1516],{},"MAX_STEPS_PER_RUN"," loop cap.",[501,1519,1521],{"id":1520},"crons-and-resilience","Crons and resilience",[448,1523,1524,1525,1527,1528,1531],{},"The walker schedules each step's next step directly via ",[456,1526,560],{},", so under normal operation no cron is needed to advance a run. The single safety-net cron is in ",[456,1529,1530],{},"apps\u002Fapi\u002Fconvex\u002Fcrons.ts",":",[785,1533,1537],{"className":1534,"code":1535,"language":1536,"meta":793,"style":793},"language-ts shiki shiki-themes github-light github-dark-dimmed","crons.interval(\n  'process pending delays',\n  { minutes: 5 },\n  internal.automations.stepWalker.processPendingDelays,\n);\n","ts",[456,1538,1539,1555,1565,1578,1584],{"__ignoreMap":793},[1540,1541,1544,1548,1552],"span",{"class":1542,"line":1543},"line",1,[1540,1545,1547],{"class":1546},"sYgZi","crons.",[1540,1549,1551],{"class":1550},"sPO5f","interval",[1540,1553,1554],{"class":1546},"(\n",[1540,1556,1558,1562],{"class":1542,"line":1557},2,[1540,1559,1561],{"class":1560},"s-HuK","  'process pending delays'",[1540,1563,1564],{"class":1546},",\n",[1540,1566,1568,1571,1575],{"class":1542,"line":1567},3,[1540,1569,1570],{"class":1546},"  { minutes: ",[1540,1572,1574],{"class":1573},"sviXB","5",[1540,1576,1577],{"class":1546}," },\n",[1540,1579,1581],{"class":1542,"line":1580},4,[1540,1582,1583],{"class":1546},"  internal.automations.stepWalker.processPendingDelays,\n",[1540,1585,1587],{"class":1542,"line":1586},5,[1540,1588,1589],{"class":1546},");\n",[448,1591,1592,1595,1596,1598,1599,1602,1603,1606,1607,1609,1610,1612,1613,1616,1617,1619],{},[456,1593,1594],{},"processPendingDelays"," queries ",[456,1597,498],{}," via the ",[456,1600,1601],{},"by_status_and_delay_until"," index for rows that are still ",[456,1604,1605],{},"pending"," with a ",[456,1608,1466],{}," in the past, and re-dispatches ",[456,1611,521],{}," for each. This catches delay steps whose original ",[456,1614,1615],{},"runAfter"," was lost — for example, if the deployment was offline when the delay elapsed. Because a re-dispatch and the original schedule could both fire, this is precisely the duplicate the atomic step claim defends against: only one of them claims the ",[456,1618,587],{}," transition; the loser drops.",[501,1621,1623],{"id":1622},"run-and-step-run-status-spaces","Run and step-run status spaces",[448,1625,1626,1628,1629,1631],{},[456,1627,490],{}," and ",[456,1630,498],{}," have their own status enums, distinct from the parent automation's lifecycle:",[613,1633,1634,1644],{},[616,1635,1636],{},[619,1637,1638,1641],{},[622,1639,1640],{},"Table",[622,1642,1643],{},"Statuses",[629,1645,1646,1660],{},[619,1647,1648,1652],{},[634,1649,1650],{},[456,1651,490],{},[634,1653,1654,775,1656,775,1658],{},[456,1655,1229],{},[456,1657,1319],{},[456,1659,669],{},[619,1661,1662,1666],{},[634,1663,1664],{},[456,1665,498],{},[634,1667,1668,775,1670,775,1672,775,1674,775,1676],{},[456,1669,1605],{},[456,1671,591],{},[456,1673,1319],{},[456,1675,610],{},[456,1677,1678],{},"skipped",[448,1680,1681,1682,1684,1685,1687,1688,1691,1692,1694,1695,1691,1697,1700,1701,1703,1704,1706],{},"These are sibling state spaces — an in-flight ",[456,1683,1229],{}," run is not a state of the parent ",[456,1686,760],{}," machine. Run completion (",[456,1689,1690],{},"completeAutomationRun",") bumps the ",[456,1693,1077],{}," shard; cancellation (",[456,1696,694],{},[456,1698,1699],{},"statsCancelled"," shard. ",[456,1702,1074],{}," is never written directly — it is derived as ",[456,1705,1264],{}," by the rollup.",[448,1708,1709,1710,1712,1713,1716,1717,1720,1721,1723,1724,1726,1727,1730,1731,1734,1735,1738,1739,1742],{},"A ",[456,1711,1678],{}," step run is terminal and written by ",[456,1714,1715],{},"markStepsSkipped",": when a condition step branches ",[484,1718,1719],{},"forward"," (its target is beyond the next sequential index), the walker records one ",[456,1722,1678],{}," step run for each bypassed step before scheduling the target. This is the only producer of ",[456,1725,1678],{}," — a backward branch or a normal ",[456,1728,1729],{},"+1"," advance bypasses nothing. The funnel (",[456,1732,1733],{},"getStepAnalytics"," \u002F ",[456,1736,1737],{},"getAutomationStats",") sums each step's ",[456,1740,1741],{},"statSkipped"," counter so the analytics reflect the steps a contact jumped over.",[501,1744,1746],{"id":1745},"related","Related",[1748,1749],"link-card",{"description":1750,"title":38,"to":37},"The product guide to building trigger-based automation workflows.",[1748,1752],{"description":1753,"title":318,"to":317},"Function types, the secure-by-default builders, and the domain-folder layout.",[1748,1755],{"description":1756,"title":222,"to":221},"The API-key route that fires event_received triggers for a contact.",[1758,1759,1760],"style",{},"html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":793,"searchDepth":1557,"depth":1557,"links":1762},[1763,1769,1773,1779,1784,1785,1786],{"id":503,"depth":1557,"text":504,"children":1764},[1765,1766,1767,1768],{"id":553,"depth":1567,"text":554},{"id":603,"depth":1567,"text":604},{"id":706,"depth":1567,"text":707},{"id":729,"depth":1567,"text":730},{"id":753,"depth":1557,"text":754,"children":1770},[1771,1772],{"id":849,"depth":1567,"text":850},{"id":953,"depth":1567,"text":954},{"id":1088,"depth":1557,"text":486,"children":1774},[1775,1776,1777,1778],{"id":1184,"depth":1567,"text":1185},{"id":1202,"depth":1567,"text":1203},{"id":1222,"depth":1567,"text":1223},{"id":1241,"depth":1567,"text":1242},{"id":1322,"depth":1557,"text":1323,"children":1780},[1781,1782,1783],{"id":1350,"depth":1567,"text":1351},{"id":1419,"depth":1567,"text":1420},{"id":1473,"depth":1567,"text":1474},{"id":1520,"depth":1557,"text":1521},{"id":1622,"depth":1557,"text":1623},{"id":1745,"depth":1557,"text":1746},"md",{},true,{"title":270,"description":271},"3.developer\u002F18.automation-internals","Eh9GY4pGbqEFDEF4K3BeUoGlb7T-XFyjyAGDDpn6YOk",[1794,1796],{"title":266,"path":265,"stem":1795,"children":-1},"3.developer\u002F17.audience-internals",{"title":274,"path":273,"stem":1797,"children":-1},"3.developer\u002F19.deliverability-infrastructure",1782846428363]