Back to Email Newsletter Production

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

Developer Handover Pack

Email Newsletter Production

[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]

This document is the primary technical reference for the FullSpec team building the Email Newsletter Production automation. It covers the current-state process map with bottleneck identification, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. Everything needed to begin development without additional discovery is contained here. Where a decision or confirmation is still outstanding, it is flagged explicitly so the build does not stall waiting for context.

01Process snapshot

Step
Name
Description
1
Brief Content Contributors
Marketing Coordinator sends individual Slack messages to writers, designers, and the social team with their assigned section, word count, and copy deadline. Time cost: 20 minutes.
2
Collect and Review Submitted Copy
BOTTLENECK. Coordinator waits for contributors to drop copy into a shared Google Doc or paste into Slack, then reads through everything for tone and length. Time cost: 40 minutes.
3
Source and Resize Images
Designer opens Canva, creates or resizes images to match newsletter template dimensions, and exports them manually. Time cost: 30 minutes.
4
Build Email in Mailchimp
BOTTLENECK. Coordinator duplicates the previous Mailchimp issue and manually pastes in new headlines, body copy, CTAs, and images section by section. Time cost: 45 minutes.
5
Check and Test All Links
Every hyperlink in the draft is clicked manually to confirm it is live and pointing to the correct destination. Time cost: 20 minutes.
6
Send Test Email to Reviewer
Coordinator sends a Mailchimp preview email to the marketing manager or business owner for final sign-off. Time cost: 10 minutes.
7
Incorporate Feedback and Edits
BOTTLENECK. Reviewer replies by Slack or email; coordinator re-enters the Mailchimp builder to make corrections manually. Time cost: 25 minutes.
8
Set Audience Segment and Schedule Send
Coordinator selects the correct subscriber segment, sets the send time, and confirms the schedule in Mailchimp. Time cost: 15 minutes.
9
Monitor Send and Log Errors
After send, coordinator checks Mailchimp for delivery errors or bounce spikes and notes them manually. Time cost: 10 minutes.
10
Pull Performance Report
Two days after send, coordinator logs into Mailchimp and Google Analytics, copies open rates, click rates, and top links into a Notion page or spreadsheet. Time cost: 25 minutes.
Time cost summary: Total manual time per cycle is 240 minutes (4 hours). At 4 newsletter issues per month this is approximately 16 hours per month and 5 hours per week. The automation replaces steps 2, 4, 5, 9, and 10 in full. Steps 1, 6, and 8 are restructured (brief sending becomes automated via Slack; approval and scheduling are gated behind a human decision node). Steps 3 and 7 remain with humans: image sourcing is not automated, and feedback incorporation is driven by the coordinator's approval choice at the Slack gate.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Content Assembly Agent

The Content Assembly Agent handles the transition from raw contributor copy to a fully populated Mailchimp campaign draft. It monitors Notion for contributor task completions and fires either when all assigned tasks are marked complete or when the configured copy deadline timestamp passes, whichever occurs first. On trigger it reads each contributor's linked Google Doc, runs a section-by-section validation pass checking character length, required fields, and completeness against the Notion brief definition, and surfaces any off-brief sections as a Slack alert before advancing to template injection. It then maps validated copy to the correct Mailchimp template variables and calls the Mailchimp Campaigns API to create a new draft. After draft creation it iterates over every href attribute in the rendered HTML to run URL HEAD checks, collects any non-200 or redirect-chain responses, and posts a flagged link report to Slack before issuing the coordinator approval notification. Estimated build time: 14 hours. Complexity: Moderate.

Trigger
All contributor Notion tasks marked complete, OR copy deadline timestamp passes, whichever comes first. Evaluated by a scheduled poller running every 15 minutes against the Notion database filtered by the current issue page ID.
Tools
Notion (task status read), Google Docs (content read via Docs API), Mailchimp (campaign create and template variable inject), Slack (missing-copy alerts and link-check report)
Replaces steps
2 (Collect and Review Submitted Copy), 4 (Build Email in Mailchimp), 5 (Check and Test All Links)
Estimated build
14 hours, Moderate
// Input
notion_page_id: string          // current issue page, e.g. 'abc123def456'
notion_task_statuses: object[]  // [{contributor_id, section_key, status, doc_url}]
copy_deadline_utc: ISO8601      // e.g. '2024-07-03T17:00:00Z'
mailchimp_template_id: string   // numeric ID of the approved Mailchimp template
mailchimp_list_id: string       // subscriber list/audience ID

// Output
mailchimp_campaign_id: string   // newly created draft campaign ID
link_check_report: object[]    // [{url, status_code, is_broken, section_key}]
validation_errors: object[]    // [{section_key, error_type, detail}] — empty if clean
slack_alert_sent: boolean       // true if any missing-copy or broken-link alert fired

// On approval (passed to scheduling step)
campaign_id: string             // same as mailchimp_campaign_id
approved_by: string             // Slack user ID of coordinator who approved
approved_at: ISO8601            // timestamp of approval interaction
  • Notion API tier: Integration token with read access to the newsletter content database. The database must have a 'Status' select property with the exact value 'Done' used to detect task completion. Confirm the property name before build; the poller filter is case-sensitive.
  • Google Docs API: Read-only OAuth 2.0 scope (https://www.googleapis.com/auth/documents.readonly). The agent reads the full document body and splits on heading markers to isolate sections. If contributors paste into a single unstructured doc, the build must define and enforce a heading convention before go-live.
  • Mailchimp template requirement: The active template must use Mailchimp's editable content blocks or a coded template with named mc:edit regions (e.g. mc:edit='headline', mc:edit='body_copy', mc:edit='cta_url'). Legacy hardcoded HTML templates cannot receive variable injection via the API and will need to be rebuilt before this agent can run.
  • Link check implementation: Use HTTP HEAD requests with a 5-second timeout per URL. Treat any non-2xx response, connection timeout, or redirect chain exceeding 3 hops as a broken link. Do not follow redirects silently; log the intermediate URLs.
  • Dedupe: The agent must check whether a campaign with the same issue_label already exists in Mailchimp before creating a new draft. If a draft for the current issue is found, update rather than duplicate, to prevent orphan campaigns accumulating on the account.
  • Fallback behaviour: If the copy deadline passes and one or more contributor tasks are still incomplete, the agent proceeds with whatever copy is available, marks missing sections with a placeholder string '[COPY MISSING: {section_key}]' in the draft, and sends a Slack alert tagging the contributor by Slack user ID. The build requires a mapping table of Notion contributor IDs to Slack user IDs.
  • Must confirm before build: (1) Mailchimp template ID and mc:edit region names; (2) Notion database ID and exact property names for Status, Section Key, and Doc URL; (3) Slack channel IDs for alerts and approval messages; (4) Contributor-to-Slack-user-ID mapping; (5) Copy deadline configuration mechanism (static weekly time or per-issue field in Notion).
Reporting Agent

The Reporting Agent runs 48 hours after a Mailchimp campaign is recorded as sent. It pulls campaign-level metrics from the Mailchimp Reports API, cross-references click-through URLs with session and referral data from the Google Analytics Data API, and writes a structured summary card to the team Notion workspace. The bounce flag logic compares the current send's bounce rate against the rolling four-week average stored in the Notion performance database; if the current rate exceeds the average, the Notion card is tagged with a 'Bounce Alert' flag and a Slack notification is sent to the marketing manager. Estimated build time: 8 hours. Complexity: Moderate.

Trigger
48 hours after the campaign.status field in Mailchimp transitions to 'sent'. Detected by a time-delayed trigger seeded when the scheduling step fires, or by a scheduled poller checking campaign status every 30 minutes and computing the elapsed delta.
Tools
Mailchimp (campaign reports API), Google Analytics (Data API, GA4 property), Notion (database write to performance card)
Replaces steps
9 (Monitor Send and Log Errors), 10 (Pull Performance Report)
Estimated build
8 hours, Moderate
// Input
mailchimp_campaign_id: string   // campaign to report on
mailchimp_sent_at: ISO8601      // timestamp used to compute the 48-hour delay
ga4_property_id: string         // GA4 property ID, e.g. 'properties/123456789'
notion_perf_db_id: string       // Notion performance database ID
bounce_avg_window_weeks: int    // default 4; rolling average window for bounce flag

// Output
notion_card_id: string          // ID of the newly created Notion performance card
open_rate: float                // e.g. 0.312 (31.2%)
click_rate: float               // e.g. 0.048 (4.8%)
top_links: object[]            // [{url, click_count}] top 3 by click_count
bounce_rate: float              // current send bounce rate
bounce_flag: boolean            // true if bounce_rate > rolling 4-week average
delivery_errors: object[]      // [{email_address, error_type}] from Mailchimp report
slack_bounce_alert_sent: boolean
  • Mailchimp Reports API: Requires the same API key used by the Content Assembly Agent. Scopes needed: read campaign reports (GET /3.0/reports/{campaign_id}), read click detail (GET /3.0/reports/{campaign_id}/click-details). Confirm the API key has reporting read access and is not restricted to campaign-write only.
  • Google Analytics Data API: Requires a service account with the Viewer role on the GA4 property. The agent queries the runReport endpoint with dimensions [pagePath, sessionSource] and metrics [sessions, bounceRate] filtered to the date range covering the 48-hour post-send window. Confirm GA4 property ID before build; Universal Analytics properties use a different API and are not supported.
  • Notion database schema for the performance card: The target database must include properties: Campaign Name (title), Send Date (date), Open Rate (number, percent format), Click Rate (number, percent format), Top Link 1 URL (url), Top Link 1 Clicks (number), Top Link 2 URL (url), Top Link 2 Clicks (number), Top Link 3 URL (url), Top Link 3 Clicks (number), Bounce Rate (number, percent format), Bounce Alert (checkbox), Delivery Errors (rich text). If the database does not yet exist, the FullSpec team will create it during the Reporting Agent build stage.
  • Rolling bounce average: Computed at runtime by querying the Notion performance database for the last four campaign cards and averaging the Bounce Rate property. If fewer than four records exist, use whatever is available and log a low-sample warning in the card.
  • Dedupe: The agent checks the Notion performance database for an existing card with a matching Campaign Name before writing. If a card exists, it updates rather than creates a duplicate.
  • Must confirm before build: (1) GA4 property ID and service account email to be granted Viewer access; (2) Notion performance database ID and confirmation that the property schema above is in place or agreed for creation; (3) Slack channel and user ID to receive bounce alerts (likely the marketing manager); (4) Whether delivery error logging should also post to Slack or remain Notion-only.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow, Email Newsletter Production automation
// ─────────────────────────────────────────────────────────────────
// TRIGGER: Weekly schedule (e.g. Monday 08:00 local time)
// ─────────────────────────────────────────────────────────────────
trigger.fired_at: ISO8601
trigger.issue_label: string          // e.g. 'Newsletter #47 – Week of Jul 7'
trigger.notion_page_id: string       // current issue page pulled from Notion template

// ACTION 1: Pull content brief from Notion
notion.get_page(notion_page_id)
  -> brief.sections: object[]        // [{section_key, contributor_id, doc_url, word_limit}]
  -> brief.copy_deadline_utc: ISO8601
  -> brief.mailchimp_template_id: string
  -> brief.mailchimp_list_id: string

// ACTION 2: Send contributor briefs via Slack
for each section in brief.sections:
  slack.post_message(
    channel: contributor_slack_id_map[section.contributor_id],
    text: 'Brief for {section.section_key}: {word_limit} words by {copy_deadline_utc}',
    blocks: [section_detail_block, doc_link_block]
  )
  notion.update_task(notion_page_id, section.contributor_id, status='Briefed')

// POLL: Every 15 min — check Notion task statuses
notion.query_database(filter: {page_id: notion_page_id, property: 'Status'})
  -> task_statuses: object[]         // [{contributor_id, section_key, status, doc_url}]
  -> all_done: boolean               // true if all statuses == 'Done'
  -> deadline_passed: boolean        // true if now() >= copy_deadline_utc

// CONDITION: Proceed when all_done == true OR deadline_passed == true

// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF 1 → Content Assembly Agent
// ─────────────────────────────────────────────────────────────────
content_assembly_agent.run({
  notion_page_id,
  notion_task_statuses: task_statuses,
  copy_deadline_utc: brief.copy_deadline_utc,
  mailchimp_template_id: brief.mailchimp_template_id,
  mailchimp_list_id: brief.mailchimp_list_id
})

// ACTION 3: Read Google Docs content per contributor
for each task in task_statuses where status == 'Done':
  google_docs.get_document(task.doc_url)
    -> raw_copy.section_key: string
    -> raw_copy.body_text: string
    -> raw_copy.char_count: int

// ACTION 4: Validate copy against brief
for each section in brief.sections:
  validate(
    body_text: raw_copy[section_key].body_text,
    char_count: raw_copy[section_key].char_count,
    word_limit: section.word_limit
  )
    -> validation_errors: object[]   // [{section_key, error_type, detail}]
if validation_errors.length > 0:
  slack.post_message(channel: coordinator_channel_id, text: validation_error_summary)

// ACTION 5: Check for missing sections (deadline-passed path)
for each section in brief.sections where task.status != 'Done':
  formatted_copy[section.section_key] = '[COPY MISSING: {section_key}]'
  slack.post_message(
    channel: contributor_slack_id_map[section.contributor_id],
    text: 'Your copy for {section_key} was not received by the deadline.'
  )

// ACTION 6: Map copy to Mailchimp template variables
mailchimp_payload = {
  type: 'regular',
  recipients: { list_id: brief.mailchimp_list_id },
  settings: {
    subject_line: formatted_copy['subject_line'],
    preview_text: formatted_copy['preview_text'],
    from_name: config.from_name,
    reply_to: config.reply_to_email,
    template_id: brief.mailchimp_template_id
  },
  content_sections: {
    'mc:edit=headline':   formatted_copy['headline'],
    'mc:edit=body_copy':  formatted_copy['body_copy'],
    'mc:edit=cta_text':   formatted_copy['cta_text'],
    'mc:edit=cta_url':    formatted_copy['cta_url'],
    'mc:edit=feature_image_url': formatted_copy['feature_image_url']
  }
}

// ACTION 7: Create Mailchimp draft campaign
mailchimp.campaigns.create(mailchimp_payload)
  -> campaign.id: string             // mailchimp_campaign_id
  -> campaign.archive_url: string    // preview URL for coordinator
  -> campaign.status: 'save'

// ACTION 8: Run automated link check
mailchimp.campaigns.content(campaign.id)
  -> rendered_html: string
extract_hrefs(rendered_html) -> urls: string[]
for each url in urls:
  http.head(url, timeout=5000)
    -> link_result: {url, status_code, redirect_chain, is_broken}
link_check_report: object[]          // [{url, status_code, is_broken, section_key}]
if any link_check_report[].is_broken == true:
  slack.post_message(channel: coordinator_channel_id, text: broken_link_summary)

// ACTION 9: Send approval notification via Slack
slack.post_message(
  channel: coordinator_channel_id,
  blocks: [
    preview_url_block: campaign.archive_url,
    approve_button: {action_id: 'approve_send', campaign_id: campaign.id},
    request_edits_button: {action_id: 'request_edits', campaign_id: campaign.id}
  ]
)

// WAIT: Coordinator interacts with Slack approval message
// Timeout window: configurable (default 4 hours); no-response = DO NOT SEND
slack.interaction_payload
  -> action_id: 'approve_send' | 'request_edits'
  -> approved_by: slack_user_id
  -> approved_at: ISO8601

// CONDITION: If action_id == 'approve_send' → proceed to schedule
// CONDITION: If action_id == 'request_edits' → notify and halt; coordinator edits manually

// ACTION 10: Schedule send in Mailchimp
mailchimp.campaigns.schedule({
  campaign_id: campaign.id,
  schedule_time: config.send_time_utc   // e.g. next Wednesday 10:00 UTC
})
  -> schedule.status: 'scheduled'
  -> schedule.send_time: ISO8601

// ─────────────────────────────────────────────────────────────────
// EVENT: Campaign status transitions to 'sent' in Mailchimp
// 48-hour delay trigger seeded at schedule confirmation
// ─────────────────────────────────────────────────────────────────

// AGENT HANDOFF 2 → Reporting Agent (fires at sent_at + 48h)
// ─────────────────────────────────────────────────────────────────
reporting_agent.run({
  mailchimp_campaign_id: campaign.id,
  mailchimp_sent_at: campaign.send_time,
  ga4_property_id: config.ga4_property_id,
  notion_perf_db_id: config.notion_perf_db_id,
  bounce_avg_window_weeks: 4
})

// ACTION 11: Pull campaign report from Mailchimp
mailchimp.reports.get(campaign.id)
  -> report.open_rate: float
  -> report.click_rate: float
  -> report.bounce_rate: float
  -> report.delivery_errors: object[]
mailchimp.reports.click_details(campaign.id)
  -> click_details: [{url, unique_clicks}]  // sorted desc
  -> top_links: click_details[0..2]          // top 3

// ACTION 12: Pull session data from Google Analytics
ga4.run_report({
  property: config.ga4_property_id,
  date_range: [sent_at, sent_at + 48h],
  dimensions: ['pagePath', 'sessionSource'],
  metrics: ['sessions', 'bounceRate'],
  filter: {sessionSource: 'email'}
})
  -> ga4_sessions: int
  -> ga4_bounce_rate: float

// ACTION 13: Compute bounce flag
notion.query_database(notion_perf_db_id, last 4 records by Send Date)
  -> historical_bounce_rates: float[]
rolling_avg = mean(historical_bounce_rates)
bounce_flag = (report.bounce_rate > rolling_avg)

// ACTION 14: Write Notion performance card
notion.pages.create({
  parent: { database_id: config.notion_perf_db_id },
  properties: {
    'Campaign Name':  { title: campaign.settings.subject_line },
    'Send Date':      { date: mailchimp_sent_at },
    'Open Rate':      { number: report.open_rate },
    'Click Rate':     { number: report.click_rate },
    'Bounce Rate':    { number: report.bounce_rate },
    'Top Link 1 URL': { url: top_links[0].url },
    'Top Link 1 Clicks': { number: top_links[0].unique_clicks },
    'Top Link 2 URL': { url: top_links[1].url },
    'Top Link 2 Clicks': { number: top_links[1].unique_clicks },
    'Top Link 3 URL': { url: top_links[2].url },
    'Top Link 3 Clicks': { number: top_links[2].unique_clicks },
    'Bounce Alert':   { checkbox: bounce_flag },
    'Delivery Errors':{ rich_text: stringify(report.delivery_errors) }
  }
})
  -> notion_card_id: string

// CONDITION: If bounce_flag == true
slack.post_message(
  channel: manager_slack_channel_id,
  text: 'Bounce alert for {campaign.settings.subject_line}: {report.bounce_rate}% vs {rolling_avg}% 4-week average.'
)

// ───────────────────────────────────────────────��─────────────────
// END OF CYCLE
// notion_card_id written, all metrics stored, cycle complete
// ─────────────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform (build tool to be confirmed by the FullSpec team). One workflow per agent, plus a shared root workflow for the trigger, brief-sending, and approval gate. All API credentials are stored in a shared credential store at the platform level, not hardcoded in individual workflow steps. Each workflow references credentials by name (e.g. 'Mailchimp-API', 'Notion-Integration-Token', 'Google-SA-Newsletter') to allow key rotation without workflow edits.
Webhook configuration
Two inbound webhooks are required. (1) Slack interactive components webhook: receives the approve/request-edits payload from the coordinator's Slack approval message. The endpoint must be registered in the Slack app manifest under 'Interactivity and Shortcuts > Request URL'. The platform's generated HTTPS webhook URL is used here; confirm URL stability before Slack app submission. (2) Optional inbound webhook from Notion automations if the polling approach is replaced with a push model: Notion's native automations can POST to the workflow URL when a task Status property changes to 'Done'.
Templating approach
Mailchimp template injection uses the Campaigns API PUT /3.0/campaigns/{campaign_id}/content endpoint with the 'template' object specifying the template_id and a 'sections' map keyed by mc:edit region name. No external templating engine is required. Dynamic values (subject line, preview text, CTA URL) are set in the campaign settings object at create time. Image URLs are injected as plain string values into the feature_image_url section; image files themselves are not uploaded by the automation (contributors supply hosted URLs or the team uses a fixed Notion image library).
Error logging
All workflow execution errors, validation failures, and link-check anomalies are written to a dedicated error log table in Supabase (table name: newsletter_automation_errors). Each row contains: run_id (uuid), workflow_name (text), step_name (text), error_type (text), error_detail (jsonb), occurred_at (timestamptz), resolved (boolean, default false). A Supabase database webhook triggers a Slack alert to the FullSpec monitoring channel (support@gofullspec.com routes to this channel) when a new unresolved error row is inserted. The coordinator is only alerted for actionable errors (missing copy, broken links, approval timeout); infrastructure errors route to FullSpec only.
Testing approach
All agent logic is developed and verified against sandbox accounts first. Mailchimp: use a dedicated test audience (minimum 1 subscriber, internal email only) and the Mailchimp test campaign mode. Google Docs: use a set of fixture documents with known content lengths and deliberate edge cases (empty section, over-limit section, missing CTA URL). Notion: use a staging database duplicated from the production schema. Google Analytics: use the GA4 sandbox property or a date-range from a historical send. Slack: use a private test channel. The full dry-run phase (two complete cycles without live sends) is defined in the Test and QA Plan document.
Estimated total build time
Content Assembly Agent: 14 hours. Reporting Agent: 8 hours. End-to-end integration testing, approval gate wiring, error logging setup, and dry-run support: 6 hours (included in the 22-hour build estimate). Total: 22 hours across a 3 to 4 week delivery window as scoped.
Canva image creation is outside the automation scope. The build handles image URL injection only. Contributors must supply correctly sized, publicly accessible image URLs, or the team must standardise on a fixed hosted image library referenced by section key. If image URLs are missing at assembly time, the feature_image_url section will be populated with a placeholder and flagged in the validation error report.
The Slack approval gate has a configurable timeout window (default 4 hours). If the coordinator does not interact with the approval message within the window, the workflow halts and does NOT proceed to schedule the send. A follow-up Slack reminder fires at the halfway point of the timeout window. The escalation behaviour (e.g. notify the marketing manager after timeout) must be agreed and configured before go-live. This rule cannot be left undefined at launch.
Item
Detail
Status
Mailchimp API key
Campaign-write and reports-read permissions required
Confirm before build
Mailchimp template ID
mc:edit region names must be documented
Confirm before build
Notion integration token
Read/write access to content brief and performance databases
Confirm before build
Notion database IDs
Content brief DB and performance card DB IDs required
Confirm before build
Google Docs OAuth
Read-only scope; service account or coordinator OAuth
Confirm before build
GA4 service account
Viewer role on the GA4 property; property ID required
Confirm before build
Slack app
Interactive components webhook URL; channel IDs for coordinator and manager
Confirm before build
Contributor-to-Slack ID map
Required for targeted missing-copy alerts
Confirm before build
Supabase error log table
FullSpec provisions this during build setup
FullSpec action
Send time configuration
Weekly send time (UTC) and approval timeout window
Owner to confirm
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Email Newsletter Production.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View