Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
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
02Agent specifications
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.
// 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).
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.
// 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.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// 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
// ─────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Email Newsletter Production.