Skip to the lesson
CivOps AI Academy · M16Email and Text Marketing: Consent, Sending, Journeys and Attribution
0%

Chapter 2 · Segments, ESP, events and flows

Audiences, sends and journeys

With the gate in place, the module can build audiences, send through an ESP, read back what happened, and run multi-step journeys. This chapter also covers the numbers that tell you whether the mail is healthy, and the one that misleads.

37 min18 mkt_ tables1 ESP webhookComplaints under 0.3%

By the end of this chapter you can

  • Define a dynamic segment, a versioned template and a send with an A/B variant.
  • Receive ESP events safely and turn bounces, complaints and unsubscribes into suppressions.
  • Describe a journey as rows (trigger, wait, branch, actions) and a contact's place in it as one enrolment row.
  • Compute deliverability and complaint rate, and explain why opens are a weak measure.

The tables you build

The specification names eighteen tables, all with the standard columns and the mkt_ prefix. Contacts, leads and opportunities belong to the CRM (module M15), so the module points at them with soft links named contact_ref, lead_ref and opportunity_ref: an id with no database foreign key, so the two modules can change independently.

The eighteen mkt_ tablesSix groups: consent and suppression, audience and tracking, templates and sends, journeys, scoring, and campaigns and money, listing the eighteen mkt_ tables. Consent and suppressionmkt_consentmkt_suppressionAudience and trackingmkt_segmentmkt_segment_membermkt_web_visitTemplates and sendsmkt_email_templatemkt_email_sendmkt_email_eventJourneysmkt_flowmkt_flow_stepmkt_flow_enrollmentScoringmkt_score_rulemkt_score_eventCampaigns and moneymkt_campaignmkt_campaign_membermkt_attribution_touchmkt_ad_accountmkt_ad_spend_daily18 tables in all; crm_contact, crm_lead and crm_opportunity are soft links, not tables you own.
The eighteen tables in six groups. The first group is built first; the money group is built last.

Segments

A segment is an audience. A dynamic segment keeps its rule in definition (JSON) and is rebuilt on a schedule, so mkt_segment_member holds the current members and member_count and refreshed_at say how fresh the count is. A static segment (is_dynamic false) is a fixed list. The rules can use contact, account, behaviour and CRM data.

A segment definition: one rule per line, all must match (illustrative)
{ "all": [
    { "field": "contact.city", "op": "in", "value": ["Cleveland", "Lakewood"] },
    { "field": "behaviour.clicked_email", "op": "within_days", "value": 30 }
] }

Templates and sends

A template is a versioned message: subject, preheader, HTML, plain-text body, a version number and a status of draft, approved or archived. Only an approved version can be sent, and an edit to an approved template creates the next version so that every send records exactly what went out. Personalisation tokens fill in the name and other fields at send time, and a preview or test send goes to a staff address first.

A send (mkt_email_send) joins a template, a segment and optionally a campaign. The variant is a, b, control or single: an A/B test is two sends from the same segment with different subjects or content, and the winner is judged on clicks or replies, not opens. The esp_batch_id links the send to the ESP's own record. Throttling, queueing and retries are the ESP's job.

Sending through an ESPThe platform sends through an ESP API, which delivers to the mailbox. The ESP calls a webhook route, which stores each event in mkt_email_event and reacts with suppression and scoring. Your platformmkt_email_sendESP APIResend, SES or SendGridRecipient mailboxthe contact's inboxsenddeliverWebhook routechecks the call is from the ESPmkt_email_eventone row per eventeventsstoreSuppression and scorebounce, complaint, clickreactYou hand the ESP a message; it handles the mail servers, the queue and the throttling.
One send and its events. The platform hands each message to the ESP. The ESP calls a webhook route in your platform for every delivery, click, bounce and complaint, and the route stores it and reacts.

Events from the ESP

Every outcome arrives as an event on mkt_email_event: delivered, open, click, bounce_hard, bounce_soft, complaint or unsubscribe, with the time, the contact, the link clicked and the ESP's own esp_event_id [1]. Three habits keep the webhook safe.

  • Check who is calling. Verify the signature or secret your ESP documents before trusting the body; an open endpoint lets anyone write fake complaints or unsubscribes.
  • Make it idempotent. ESPs retry. Store the esp_event_id and ignore one you have already seen, so a retry does not count twice.
  • Act on the event. A hard bounce, a complaint or an unsubscribe writes a mkt_suppression row at once (and an unsubscribe or a complaint also adds a withdrawn consent row; an unsubscribe publishes mkt.contact.unsubscribed). A click can raise a lead score (chapter 3).

Is the mail healthy?

KPIDefinitionUse
DeliverabilityDelivered ÷ sentFalling deliverability means bounces: clean the list and check authentication
Complaint rateComplaints ÷ delivered. Keep under 0.3%; aim for under 0.1%State your denominator once and keep it; mailbox providers publish their own thresholds [2][3]
Click-to-open rateUnique clicks ÷ unique opensOnly as good as the open count it divides
Complaint rate to scaleA scale from 0 to 0.5 percent. The target bar ends at 0.1 percent and the keep-under bar at 0.3 percent, drawn to scale. 0.0%0.1%0.2%0.3%0.4%0.5%Target: under 0.1% (1 complaint in 1,000 delivered)Keep under 0.3% (3 complaints in 1,000 delivered)Above this line: pause sends and look at who you are mailing
The complaint-rate limits, to scale. The band between the green and amber bars is the room you have: a rate up to but not including three times the target is still allowed, but only just.

Journeys

A journey (mkt_flow) runs when a trigger fires: a form submission, a page visit, a score reaching a threshold, a field change or a date. Its steps are rows in mkt_flow_step, each with a step_type (send_email, wait, branch, update_field, add_to_segment, notify_owner, create_crm_task, webhook or exit), a JSON config and the number of the next step. A branch has two next steps, one for true and one for false.

A journey: trigger, wait, branchA form submission triggers a welcome email, then a two-day wait, then a branch. If the contact clicked, the owner is notified and a CRM task is created; if not, a reminder is sent and the journey exits. The contact's place is one mkt_flow_enrollment row. Triggerform submittedSend emailwelcome messageWait2 daysBranchclicked a link?Yes: notify ownerand create a CRM taskNo: send reminderthen exittruefalsemkt_flow_enrollmentcurrent_step_no, next_run_atOne row per contact marks their place.Each box is a row in mkt_flow_step; branch_true_step_no and branch_false_step_no point at the next rows.
A two-step journey. The wait step stores the time to wake in next_run_at; a worker runs every enrolment whose time has come.

Each contact in a journey has one mkt_flow_enrollment row: the current_step_no, a status of active, completed, exited or errored, and next_run_at. Three rules keep journeys safe.

  • Every send step uses the gate. A contact who unsubscribed on day 2 must not get the reminder on day 4. The journey calls the same recipient function as a campaign.
  • Re-entry is a choice. reentry_allowed says whether a contact may start the same journey twice; without it, a form submitted twice sends two welcome series.
  • Finishing is an event. A journey that completes publishes mkt.flow.completed, so other modules can react.
Exercise · Send one email and watch the events arrive30 minutes

You need: Your platform with the gate from chapter 1, an ESP account in test or sandbox mode with a verified sending domain, and two staff addresses you control

Use two staff addresses only. Do not send to your real list in this exercise. Check your ESP's pricing page for what a real send costs before you choose one, and prefer a plan with a free allowance if there is one.

Outcome: A working path from segment to template to send to events, with a passing replay test and a proven unsubscribe, using only two staff addresses.

Knowledge check

The ESP calls your webhook twice with the same esp_event_id. What should happen?

Knowledge check

Why is a test winner judged on clicks and not opens?

Knowledge check

A journey sends a reminder on day 4. The contact unsubscribed on day 2. What stops the reminder?

References

  1. Resend documentation: sending email and webhooks. https://resend.com/docs
  2. Yahoo Sender Hub: sender best practices. https://senders.yahooinc.com/best-practices/
  3. Google: Email sender guidelines. https://support.google.com/a/answer/81126
  4. IETF RFC 8058: Signaling One-Click Functionality for List Email Headers. https://www.rfc-editor.org/rfc/rfc8058

Chapter 3 · From first click to won job

Scoring, spend and attribution

The last labs connect marketing to money: which leads are ready for a person to call, what each channel costs, and which touches deserve credit for a won job. This chapter also covers moving your data in without losing consent and cutting over.

35 minFit + engagement4 attribution modelsTotals reconcile to won revenue

By the end of this chapter you can

  • Write scoring rules with points and decay, and hand a lead to the CRM owner at a threshold.
  • Ingest daily ad spend and compute cost per lead, MQL and won job for a stated model.
  • Credit a won job under first-touch, last-touch, linear and W-shaped models so each reconciles to the revenue in whole cents.
  • Import contacts only with their consent provenance and retire the old subscription with the saving recorded.

Tracking, with consent

First-party tracking records a visit in mkt_web_visit: an anonymous_id, the URL, the referrer, the UTM parameters (utm_source, utm_medium, utm_campaign and the rest) and a session. When a visitor submits a form (the event cms.web_form.submitted), the visit history is attached to the new or existing contact, and the UTM values are kept on the lead so the first source is never lost. Where the rules in chapter 1 require consent for tracking, nothing is recorded until the visitor has accepted tracking: for a known contact, a tracking consent row says granted; before a visitor is known, their banner choice is held in a first-party cookie (a small note your own site stores in their browser) that the tracking route checks, and it is written to mkt_consent when they identify themselves.

Lead scoring and the MQL hand-off

A score ranks the people worth a phone call. Rules live in mkt_score_rule with a rule_type of fit (who they are: in the service area, owns the building), engagement (what they did: submitted a form, visited the pricing page, clicked) or negative (a competitor, a bounced address). Each rule has points and an optional decay_days. When a rule matches, a row is written to mkt_score_event with the points and an expires_at. A contact's score is the sum of their events that have not expired; no total is stored or edited, the same append-only idea as the ledger.

A lead score rising and decayingA score line over thirty days: fit points of 15 stay, a form adds 10 on day 2, a pricing page visit adds 10 on day 5 and crosses the threshold of 30 so the lead is handed to the CRM owner. The engagement points expire on days 16 and 19 and the score returns to 15. Example values, drawn to scale. day 0day 5day 10day 15day 20day 25day 30MQL threshold: 30 (example)MQL: hand off to the CRM owner+10 pricing page visit+10 form+15 fit, never expires+10 expires+10 expires: back to fit only
A score over thirty days, with example rules. Fit points stay. Engagement points expire, so a lead who goes quiet falls back below the threshold without anyone editing a number.

When the score first reaches the MQL (marketing-qualified lead) threshold, the platform publishes mkt.lead.mql, assigns the contact to the CRM owner by the CRM's routing rule, and can create a CRM task through a journey step. The KPI is MQL to SQL conversion: SQLs (sales-qualified leads) divided by MQLs. If few MQLs become SQLs, the threshold or the rules are wrong, and the fix is to change a rule row and a number, not code [1].

Campaigns, budget and paid media

A campaign (mkt_campaign) has a type (email, nurture, event, webinar, paid_social, paid_search, content, trade_show or referral), a status, dates, a budget, an actual_spend and a utm_campaign value that ties web visits to it. Money is whole cents in the database, as everywhere in the platform. Members (mkt_campaign_member) move from targeted to sent, responded and converted.

Paid media adds the cost side. An ad account (mkt_ad_account) is a connected account on LinkedIn, Meta, Google, TikTok or Microsoft. Each day, an ingestion job writes one mkt_ad_spend_daily row per account, campaign and date with spend, impressions, clicks and conversions. Make the write an upsert on those three keys so a re-run replaces the day instead of doubling it. When an opportunity is won (crm.opportunity.won), the platform could later upload the won deal back to the ad platform as an offline conversion, so the platform learns which clicks became jobs. That upload, and rolling child campaigns up to a parent campaign, are later work and not built in this course.

Attribution: who gets the credit

Attribution splits the value of a won job across the touches that led to it. A touch is a row in mkt_attribution_touch with a channel, a touch_type (first, lead_creation, opportunity_creation, close or other) and a time. Four models are taught, and none of them is the truth; each is a stated convention.

  • First touch: all credit to the first touch. It answers: where do people first find us?
  • Last touch: all credit to the last touch before the sale. It answers: what closes?
  • Linear: equal credit to every touch.
  • W-shaped: a common convention gives 30% each to the first touch, the lead-creation touch and the opportunity-creation touch, and shares the remaining 10% among the other touches. Your platform states its own weights and uses them everywhere.
One won job, four attribution modelsA won job worth 10,000 dollars with four touches: paid search, web form, email and phone call. First touch gives all of it to paid search, last touch to the phone call, linear 2,500 to each, and W-shaped 3,000, 3,000, 1,000 and 3,000. Every row totals 10,000. Paid search · firstWeb form · leadEmail · otherPhone call · opp.First touch$10,000Last touch$10,000Linear$2,500$2,500$2,500$2,500W-shaped$3,000$3,000$1,000$3,000Every row adds up to the $10,000 won. The model changes only who gets the credit.
The same $10,000 won job, four ways, drawn to scale. The total never changes; only who is credited does.

The credit is stored in the credit JSON of each touch, in whole cents. Dividing cents can leave a remainder: three equal shares of $10,000.00 are 333,333 cents each, which makes 999,999, so one cent is left. Give leftover cents to the last touch, always the same way. Then each model's credit adds up to the won revenue exactly, which is the third definition-of-done check for this module: attribution totals reconcile to won revenue for each model.

KPIDefinition
Cost per lead / MQL / won job by channelChannel spend ÷ the outcomes credited to that channel, with the attribution model stated beside the figure
MQL to SQL conversionSQLs ÷ MQLs

Move the data in, and cut over

Migration has one hard rule: never import a contact without its consent provenance. Export contacts from the old tool with their consent status, lists, templates and engagement history. Load the suppressions first. A contact with no record of where and when they agreed is imported as not consented and is not mailed until they opt in again. Where consent for EU residents is exchanged with an advertising or consent platform, the IAB Europe Transparency and Consent Framework defines a standard consent string [4].

Cutover follows the same pattern as the other modules: run the old tool and the new platform side by side, compare, then cancel. Before you do, check that the complaint rate on the new platform is under 0.3%, that the attribution totals reconcile to won revenue under each model, and that a suppressed address cannot be sent to. Then cancel the old subscription and record the saving from the invoice.

Exercise · Credit one won job four ways25 minutes

You need: Your platform with mkt_campaign and mkt_attribution_touch, one real won job from your CRM, and a calculator

Use one job that was really won and the contact's real history. If you have no tracking data yet, use the contact's form submission, the first call and the estimate visit from your notes.

Outcome: One won job credited under four models with every total reconciled to the cent, and a cost per won job printed with its model.

Knowledge check

A contact's fit rule gives +20 with no decay_days, and a form rule gives +10 with decay_days of 14. What is the score 20 days after both?

Knowledge check

$1,000.00 is credited linearly across three touches. Which split keeps the total exact?

Knowledge check

You import a list from your old tool and 400 contacts have no record of where or when they agreed. What do you do?

References

  1. Improvado: B2B marketing automation platforms (scoring, attribution). https://improvado.io/blog/marketing-automation-tools
  2. Abmatic: best B2B marketing automation platforms 2026. https://abmatic.ai/blog/best-marketing-automation-platforms-b2b-2026
  3. Airframe: marketing automation platform category definition. https://www.airframe.ai/market-intelligence/markets/v4--marketing-automation-platforms--marketing
  4. IAB Europe: Transparency and Consent Framework. https://iabeurope.eu/transparency-consent-framework/

Chapter 4 · 12 questions · 80% passes

Final assessment

Twelve questions across the element. Score 80% (10 of 12) to pass. Your LMS records your score and each answer; you can review the chapters and try again.

15 min12 questions≈ 15 minutesRetake allowed

Choose one answer for each question, then submit. You will see the right answer and why for every question.

1. What must be true of an address before it is included in a send?
2. Why does mkt_suppression hold one row per email address, with the address unique?
3. A contact withdraws consent for marketing email. What happens in mkt_consent?
4. Why does the module send through an ESP API instead of running its own mail server?
5. A reader presses the Unsubscribe button their mail app shows next to the sender. What does RFC 8058 describe?
6. What does a DMARC check ask?
7. A send is delivered to 5,000 contacts and draws 10 complaints. Where does the complaint rate stand?
8. Why should opens not be the success metric for a campaign?
9. How does the webhook route avoid counting a retried ESP event twice?
10. In a journey, how does a contact wait two days before the next step?
11. A lead's engagement points carry decay_days of 14 and its fit points have none. What happens to a lead who goes quiet?
12. What must be true of the credit under each attribution model for one won job?