Echo/Docs

CLI command

lifecycle

Lifecycle triggers, win-back, contact frequency and email suppressions: a product event that enrols the person who sent it, once, in an email-sequencing-cli sequence, until an exit event ends it; rules that enrol people who went quiet, once, and count who came back; one cap on how many messages any recipient may get in a window of hours, whatever the channel; and the addresses Echo must not email.

Invocation#

Shell
echo-cli lifecycle triggers list --product <id>
echo-cli lifecycle triggers add --product <id> --name <text> --app <web or mobile source> --event <event name> --sequence <sequence id> [--exit <event name>] --by <name>
echo-cli lifecycle triggers remove --id <trigger-id>
echo-cli lifecycle run --product <id> --days <n>
echo-cli lifecycle enrollments --trigger <trigger-id>
echo-cli lifecycle export --trigger <trigger-id>
echo-cli lifecycle winback list --product <id>
echo-cli lifecycle winback add --product <id> --name <text> --app <web or mobile source> (--inactive-days <n> | --segment <segment-id>) --seen-days <n> --trigger <trigger-id> --by <name>
echo-cli lifecycle winback remove --rule <rule-id>
echo-cli lifecycle winback run --product <id>
echo-cli lifecycle winback enrollments --rule <rule-id>
echo-cli lifecycle caps set --max-messages <n> --window-hours <n> --by <name>
echo-cli lifecycle caps remove
echo-cli lifecycle caps deferrals --since <ISO instant>
echo-cli lifecycle caps sends --recipient <email or contact:id>
echo-cli lifecycle suppressions list
echo-cli lifecycle suppressions history
echo-cli lifecycle suppressions check --address <email>
echo-cli lifecycle suppressions add --address <email> --reason hard_bounce|complaint|invalid_address|inactive|operator --source <text> [--detail <text>] --by <name>
echo-cli lifecycle suppressions lift --address <email> --by <name>
echo-cli lifecycle suppressions validate [--address <email>] --by <name>
echo-cli lifecycle suppressions inactive --days <n> --messages <n> [--by <name>]

Inputs and output#

triggers list writes the product's triggers by name: id, product_id, name, app_id, event_name, sequence_id, exit_event_name (or null), created_by and created_at; add and remove write the trigger they made or removed. run writes since (the start of the window read) and runs, one per trigger with trigger_id, name, enrolled (the enrolments it made) and ended (the enrolments it ended). enrollments writes a trigger's enrolments oldest first: id, trigger_id, sequence_id, subject, email (or null), enrolled_by_event, enrolled_at, ended_by_event, ended_at (both null while active) and recorded_at. export writes the same enrolments as email-sequencing-cli enrollments: id, sequenceId, contact { id, email, variables }, enrolledAt, status (active, or cancelled once ended) and events. winback list writes the product's rules by name (id, product_id, name, app_id, inactive_days and segment_id — one of them set, the other null — seen_days, trigger_id, created_by, created_at); add and remove write the rule. winback run writes runs, one per rule with rule_id, name, inactive (how many people are quiet now), enrolled and reactivated (the win-back enrolments it made and marked). winback enrollments writes enrollments, newest first (id, rule_id, subject, last_seen_at, enrollment_id, enrolled_at, reactivated_at, reactivated_by_event), enrolled and reactivated. caps set and remove write the cap they set or removed: id, max_messages, window_hours, set_by and set_at. caps deferrals writes cap (or null) and deferrals since the instant, newest first: id, recipient, channel, source, max_messages, window_hours, count (the messages already counted when the send was stopped) and deferred_at. caps sends writes cap (or null) and sends, the recipient's counted messages newest first: id, recipient, channel, source and sent_at. suppressions list writes the suppressions in force, newest first, and history every suppression with the lifted ones: id, address, reason, source, detail (or null), suppressed_by, suppressed_at, lifted_by and lifted_at (both null while in force); check writes the suppression in force for the address, or null; add and lift write the suppression they made or lifted.

State effect#

Every verb calls Echo Web's authenticated /api/analytics/lifecycle/triggers, /run, /enrollments, /winback, /winback/run, /caps or /suppressions with ECHO_INTERNAL_BEARER at ECHO_WEB_BASE_URL; the Lifecycle triggers, Win-back, Contact frequency and Email suppressions sections of the tracking screen call the same operations, as does Echo Desktop's Contact frequency screen for caps, suppressions, win-back and lifecycle triggers, and echo-mcp reads triggers (echo_list_lifecycle_triggers), enrolments (echo_lifecycle_enrollments), win-back rules (echo_list_winback_rules), their enrolments (echo_winback_enrollments), the frequency cap with its deferrals (echo_frequency_cap_deferrals), one recipient's counted messages (echo_recipient_sends), the suppressions (echo_list_suppressions) and one address's suppression (echo_check_suppression) but changes nothing. A run reads the source's stored events of the window — a web source's web events or a mobile app's mobile events — and joins every identifier the identity edges and the events link into one person. Each person who sent the trigger's event and is not yet enrolled is enrolled once, at their first such event, named by the user id their events carry, else their anonymous id, with that user's email when Echo's sign-in knows it; an enrolment whose person later sends the exit event is ended by it. A repeated event never enrols twice: Echo Web skips people already enrolled and the store refuses a second enrolment of the same subject by the same trigger. Echo Web also runs every product's triggers hourly at /api/cron/lifecycle-triggers over the last ECHO_LIFECYCLE_WINDOW_DAYS days, which the deployment declares. The sequence itself (its steps and templates) lives in an email-sequencing-cli sequence file whose id the trigger names; pipe export into a file and run email-sequencing plan --sequence <sequence.json> --enrollments <file> to see the steps due. A win-back rule reads its source's web or mobile events over the last --seen-days days; a person seen there whose last event is older than --inactive-days days — or, for a rule naming --segment instead, a person who is a member now of that behavioural segment of the same product and source, as echo-cli segments members reads it (a segment of at most 0 logins in 14 days, say) — is enrolled in the sequence of the rule's --trigger once ever, the enrolment recorded under that trigger with enrolled_by_event winback:<rule id>, so export carries it. When an enrolled person later sends any event of the source, the win-back enrolment is marked reactivated with that event and the lifecycle enrolment ends with it. Echo Web runs every product's rules daily at /api/cron/winback; a trigger a win-back rule uses cannot be removed until the rule is, nor can a segment one names. There is at most one frequency cap; beside it, a recipient may choose their own limit on the email preference page (a number of messages in a number of hours, kept in contact_frequency_choices and shown with their consents by echo-cli lifecycle consent show). With neither, nothing is deferred. Every campaign message (email or DM), every announcement email and every newsletter email Echo sends is counted for its recipient, under their email address in lower case when known, else contact:<CRM contact id>, so one person's emails and DMs count together; only a send that returned no error is counted. Before a send, the recipient's counted messages of the last window_hours hours are compared with the cap's max_messages, and then those of their own choice's hours with the number they chose (a contact:<id> recipient has no address to choose with): at or over either, the send waits and a deferral is stored with that limit and the count, its reason naming "the cap" or "their own choice (<source>, <chosen_at>)". A deferred campaign recipient keeps its status and is tried again on a later campaign tick; a deferred announcement or newsletter delivery records the deferral as its error. Setting the cap again replaces it; removing it stops deferring by the cap from then on and keeps the counted messages and deferrals, which are personal stores echo-cli privacy erase reaches. An address is suppressed in lower case, at most once at a time; before a campaign email or an announcement email, Echo asks whether its address is suppressed, and a suppressed one is not emailed: the campaign recipient is skipped and the announcement delivery records why, both with the sentence "<address> is suppressed since <instant> (<reason> from <source>[: <detail>]); echo-cli lifecycle suppressions lift --address <address> --by <name> lets mail reach it again". Lifting keeps the row with who lifted it and when, and mail reaches the address again from then on. Suppressions are not personal stores privacy erase removes, because erasing one would let mail reach the address again.

Refusals#

  • A wrong invocation is refused before anything is sent: "lifecycle takes <every usage, separated by semicolons>; got <words>", "lifecycle <verb> does not take <flag>; it takes …" (or "it takes no flags"), "<flag> needs a value", "lifecycle <verb> needs <flag>" for each flag the verb needs, "lifecycle winback takes <every win-back usage>; got <word>", "lifecycle caps takes <every caps usage>; got <word>", "lifecycle suppressions takes <every suppressions usage>; got <word>", "--inactive-days must be a whole number of days; got <value>" (and --seen-days), "--max-messages must be a whole number of messages; got <value>" and "--window-hours must be a whole number of hours; got <value>".
  • Echo Web answers a bad request with "product_id is required: …", "name is required: …", "app_id is required: …", "event_name is required: …", "sequence_id is required: …", "exit_event_name <event> is the event that enrols; an enrolment cannot end on the event that starts it", "days must be a positive whole number: the window to read", "trigger_id is required: …" or "created_by is required: …"; not found with "no product has id <id>; echo-cli products lists the ones that exist" or "no lifecycle trigger has id <id>; echo-cli lifecycle triggers list --product <id> lists them"; and a conflict with "product <id> already has the lifecycle trigger <name> as <id>". A sign-in read that fails stops the run with "the signed-in user <id> could not be read: <cause>". The hourly run without its window answers "ECHO_LIFECYCLE_WINDOW_DAYS is required" or "ECHO_LIFECYCLE_WINDOW_DAYS must be a positive whole number of days, not <value>" as unavailable. The command prints Echo Web's sentence and exits 1.
  • A win-back rule is refused with a bad request: "state exactly one of inactive_days (how long a person sent nothing before they are won back) or segment_id (the behavioural segment whose members count as inactive)", "inactive_days must be a whole number of days of at least 1: …; got <value>" (and seen_days), "seen_days must be more than inactive_days, so a person seen before they went quiet can be found; got <seen> and <inactive>", "segment <id> reads <source> of product <other>; a win-back rule's segment must read its own source, <source> of product <product>", "lifecycle trigger <id> belongs to product <other>, not <product>" or "product_id or rule_id is required: …"; not found with "no win-back rule has id <id>; echo-cli lifecycle winback list --product <id> lists them" or "no segment has id <id>; echo-cli segments list --product <id> lists them"; a conflict with "product <id> already has a win-back rule named <name>, <id>". Removing a trigger a rule uses is refused with "lifecycle trigger <id> carries the win-back rules <name> (<id>); remove them first with echo-cli lifecycle winback remove".
  • A frequency cap is refused with a bad request: "max_messages must be a whole number of at least 1: the most messages a recipient may get in one window; got <value>", "window_hours must be a whole number of at least 1: the hours each window spans; got <value>", "set_by is required: an internal caller has no signed-in user, so it names who is acting" or "since must be an ISO instant; got \"<value>\""; caps remove with nothing set is not found: "no frequency cap is set; echo-cli lifecycle caps set sets one". The command prints Echo Web's sentence and exits 1.
  • A suppression is refused with a bad request: "address must be one email address, a local part and a domain around one @; got <value>", "reason must be one of hard_bounce, complaint, invalid_address, inactive, operator; got <value>", "source is required: where the suppression came from, such as the bounce report or the operator", "detail is required: what the bounce or complaint said, when given" for a blank --detail, "suppressed_by is required: …" or "lifted_by is required: …"; a conflict with "<address> is already suppressed since <instant> (<reason> from <source>), as <id>"; and lift of an address not suppressed is not found: "<address> is not suppressed; echo-cli lifecycle suppressions list shows the addresses that are". The command prints Echo Web's sentence and exits 1.
  • suppressions validate calls /api/analytics/lifecycle/suppressions/validate (the Email suppressions section of the tracking screen and Echo Desktop's suppressions section do the same) and writes checks, one per address: address, domain, deliverable, reading (what DNS said) and suppression (the one in force, or null). With --address it checks that address; without, every distinct CRM contact address not suppressed. DNS is read now: a domain whose MX records name hosts receives mail; a null MX (one MX whose exchange is ".", RFC 7505) receives none; a domain without MX receives mail at its own A or AAAA address (RFC 5321 §5.1); one with none of them receives none. An address that receives no mail and is not suppressed is suppressed with reason invalid_address, source dns and the reading as detail, so campaign and announcement emails skip it. A DNS failure other than a missing name or record leaves deliverable null with "the <MX|A|AAAA> records of <domain> could not be read from DNS: <cause>" and suppresses nothing. Echo Web refuses an address that is not one with the sentence above and a missing --by with "checked_by is required: …".
  • suppressions inactive reads, with GET /api/analytics/lifecycle/suppressions/inactive, every address Echo emailed at least --messages times in the last --days days (every email counted in contact_sends, whatever sent it) without a click on a tracked campaign link (crm_email_clicks) or an inbound reply (crm_messages) in those days, and writes { since, days, messages, inactive }, each with address, emails, first_sent_at, last_sent_at and suppression (the one in force, or null), most emailed first; it stores nothing. With --by it POSTs instead and suppresses each found address not already suppressed with reason inactive, source "inactivity rule: at least <messages> emails in <days> days without a click or reply" and the counts as detail. The Email suppressions section of the tracking screen and Echo Desktop's address validation section do the same. Echo Web refuses "days must be a positive whole number: the window to read" and "messages must be a positive whole number: how many emails an address got in the window without a click or reply before it counts as inactive; got <value>"; the command first refuses a missing --days or --messages with the usage status.
  • Bounces and complaints reach the list without anyone recording them: Resend's webhook, pointed at Echo Web's /api/drains/email with its email.bounced and email.complained events, posts each event signed in svix-id, svix-timestamp and svix-signature with the webhook's signing secret, which the deployment holds as ECHO_RESEND_WEBHOOK_SECRET (the whsec_ value Resend shows). A permanent bounce suppresses every recipient in data.to with reason hard_bounce, a complaint with reason complaint, each with source "resend <event type>", detail "email <email id>[: <the bounce message>]" and suppressed_by "resend webhook"; an address already suppressed stays as it is, so a retried delivery suppresses once. The answer is { event: { type, email_id, suppressed (address, suppression_id, already_suppressed), ignored } }: a transient bounce and any other event suppress no one, with ignored saying why. A delivery is refused with 403 "svix-id, svix-timestamp and svix-signature are required: Resend signs every webhook delivery with them" or "svix-signature does not match the body signed with ECHO_RESEND_WEBHOOK_SECRET; the webhook's secret and Echo's differ", and as a bad request with "the webhook body is not JSON: <cause>", "the webhook body is not a Resend event: it has no type" or "the <type> event of email <id> names no recipient in data.to"; without ECHO_RESEND_WEBHOOK_SECRET, or with one that does not start with whsec_, no delivery is read and the error names the setting. A failed suppression answers an error, so Resend delivers the event again.