Back to Content Calendar Management

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

Content Calendar Management

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

This document gives the FullSpec build team everything needed to construct, connect, and validate the three-agent Content Calendar Management automation. It covers the current-state step map with bottleneck flags, full agent specifications with IO contracts, an end-to-end data flow trace, and the recommended build stack. The process owner is not expected to action anything here. All build, integration, and testing work is handled by FullSpec. Questions should be directed to support@gofullspec.com.

01Process snapshot

Step
Name
Description
1
Update Content Calendar Spreadsheet
Marketing Manager opens the shared Google Sheet and adds or updates content slots, topics, deadlines, and assigned writers for the coming week or month. Tool: Google Sheets. Time cost: 30 min/cycle.
2
Write and Send Content Briefs
Manager manually writes a brief for each piece and sends it to the assigned writer via Slack, often copy-pasting details from the calendar. Tool: Slack. Time cost: 25 min/cycle.
3
Chase Overdue Drafts
When a deadline passes with no submission, the manager checks the calendar, identifies the overdue piece, and messages the writer to follow up. BOTTLENECK: reactive, after-the-fact. Tool: Slack. Time cost: 20 min/cycle.
4
Review Submitted Draft
Reviewer locates the Google Doc shared in Slack, reads the draft, and leaves inline comments or approval notes. Tool: Google Docs. Time cost: 35 min/cycle.
5
Notify Writer of Revision or Approval
Manager sends a Slack message telling the writer whether changes are needed or the piece is approved. Tool: Slack. Time cost: 10 min/cycle.
6
Copy Approved Content Into Scheduler
Coordinator copies approved copy, images, and metadata from the Google Doc into Buffer and sets the date and time manually. BOTTLENECK: high-repetition, zero-judgment copy-paste. Tool: Buffer. Time cost: 20 min/cycle.
7
Update Calendar Status Manually
Manager returns to Google Sheets and updates the status column from Approved to Scheduled or Published. Tool: Google Sheets. Time cost: 8 min/cycle.
8
Pull Performance Data Into Spreadsheet
Manager manually exports or copies engagement metrics from each channel into the spreadsheet to compare content performance. BOTTLENECK: high-volume export work deferred until period end. Tool: Google Sheets. Time cost: 40 min/cycle.
9
Update CRM Contact Records With Content Touches
Manager manually logs the publish date and channel in HubSpot so the campaign record reflects current activity. Tool: HubSpot. Time cost: 15 min/cycle.
Time cost summary: Total manual time per content piece cycle is 203 minutes (3 hours 23 min). At the current volume of 20 to 40 pieces per month the process consumes approximately 6 hours per week, costing an estimated $9,100 per year at $32/hour. The automation replaces steps 1, 2, 3 (Calendar Orchestrator Agent), steps 5, 6, 7, 9 (Review and Publish Agent), and step 8 (Performance Sync Agent). Step 4 (human draft review) is retained. Steps 1 and 2 are absorbed into Notion as the new source of truth; the brief is generated from Notion fields rather than typed manually.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Calendar Orchestrator Agent

Watches the Notion content database for two distinct event types: a new content row being created, and an existing row reaching its deadline date field without a draft link present. On a new-row event it reads the topic, format, target audience, assigned writer, channel, and deadline fields and composes a structured brief message, then dispatches it to the assigned writer's Slack handle via direct message. On a deadline event it checks whether the draft_link field is populated; if not, it sends an overdue reminder to the writer and posts a copy to the marketing manager's Slack DM. No human input is required after the Notion record is created.

Trigger
New row created in Notion content database OR deadline date field reached on an existing row where draft_link is null
Tools
Notion (database read), Slack (direct message send)
Replaces steps
Steps 1, 2, 3 (calendar update, brief writing, overdue chasing)
Estimated build
10 hours, Moderate complexity
// Input
notion.database_id: string          // Confirmed Notion DB ID, set in credential store
notion.row.title: string             // Content piece title or working headline
notion.row.topic: string             // Topic or keyword target
notion.row.format: string            // e.g. 'Blog post', 'LinkedIn carousel', 'Short video'
notion.row.channel: string           // Target channel: LinkedIn, Instagram, Blog, etc.
notion.row.assigned_writer: string   // Notion person field, maps to Slack user ID via lookup table
notion.row.deadline: date            // ISO 8601 date, used for reminder trigger
notion.row.draft_link: url | null    // Null triggers overdue reminder path

// Output
slack.dm.to: writer_slack_id         // Resolved from Notion person -> Slack lookup
slack.dm.message: formatted_brief    // Structured block with title, format, channel, deadline
slack.dm.to: manager_slack_id        // Copied on overdue reminder only
slack.dm.message: overdue_reminder   // Includes content title, deadline, and direct Notion link
  • Notion plan must be Team tier or above to enable API access. Confirm the workspace plan before build starts.
  • The Notion database must have standardised field names before any mapping is done: 'title', 'topic', 'format', 'channel', 'assigned_writer' (person), 'deadline' (date), 'draft_link' (URL), 'status' (select). A messy or inconsistently named database is the primary build delay risk.
  • The Notion-to-Slack user ID lookup must be built as a static mapping table in the credential store during Notion audit week. It does not resolve dynamically at runtime.
  • The deadline reminder must fire on the exact deadline date at 09:00 in the workspace's local timezone, not UTC, unless the team confirms UTC is acceptable.
  • Dedupe: if the Notion row is updated multiple times on the same day (e.g. deadline field edited), the agent must check for a sent_brief flag on the record to avoid duplicate brief messages. Set a boolean custom property 'brief_sent' in Notion on first dispatch.
  • Overdue reminder cadence: one reminder on deadline day, one 24 hours later if draft_link is still null. Do not send more than two reminders per piece without manager confirmation.
  • Confirm with the team whether briefs should also be posted to a shared Slack channel (e.g. #content-briefs) in addition to the writer DM, or DM only.
Review and Publish Agent

Monitors the Notion content database for two conditions: a Google Docs URL appearing in the draft_link field on a row whose status is not yet Approved, and a row status field changing to Approved. On draft submission it sends a Slack direct message to the assigned reviewer with the document link and the content title. When the reviewer marks the Notion record as Approved, the agent reads the approved copy text, target channel, and scheduled publish datetime from Notion, creates the corresponding post in Buffer, then writes the status back to Notion as Scheduled and creates or updates the linked HubSpot campaign record with the publish date and channel. If the reviewer sets status to Needs Revision the agent sends a Slack message back to the writer with the reviewer's comment field from Notion.

Trigger
draft_link field populated in Notion (review routing path) OR status field changes to 'Approved' (publish path) OR status field changes to 'Needs Revision' (revision path)
Tools
Notion (database read and write), Slack (direct message send), Buffer (post create), HubSpot (campaign record create/update)
Replaces steps
Steps 5, 6, 7, 9 (approval notification, Buffer scheduling, status update, HubSpot logging)
Estimated build
12 hours, Complex complexity
// Input (review routing path)
notion.row.draft_link: url            // Google Docs URL logged by writer
notion.row.title: string
notion.row.assigned_reviewer: string  // Notion person field -> Slack user ID

// Output (review routing path)
slack.dm.to: reviewer_slack_id
slack.dm.message: review_request      // Includes title, draft_link, and Notion record link

// Input (publish path)
notion.row.status: 'Approved'
notion.row.approved_copy: string      // Text body of the post, stored in Notion rich text field
notion.row.channel: string            // Must map to a configured Buffer profile ID
notion.row.publish_datetime: datetime // ISO 8601; used as Buffer scheduled_at value
notion.row.hubspot_campaign_id: string | null  // Optional; if null, a new campaign is created

// Output (publish path)
buffer.post.profile_id: string        // Resolved from channel name -> Buffer profile ID map
buffer.post.text: string              // Pulled from approved_copy field
buffer.post.scheduled_at: datetime
buffer.post.id: string                // Stored back to notion.row.buffer_post_id
notion.row.status: 'Scheduled'
hubspot.campaign.publish_date: date
hubspot.campaign.channel: string
hubspot.campaign.notion_record_url: url

// On approval (revision path)
slack.dm.to: writer_slack_id
slack.dm.message: revision_notice     // Includes title, reviewer comment from Notion, record link
  • Buffer API requires an Essentials plan or above to access the scheduling endpoint. Confirm the active Buffer tier before build. The team plan or above is needed for multi-channel access.
  • The channel-to-Buffer-profile-ID mapping must be hardcoded in the credential store during initial setup. Buffer does not return profile names in a lookup-friendly format at runtime.
  • HubSpot integration uses the Campaigns API (Marketing Hub Starter or above required). Confirm HubSpot subscription tier. If no campaign exists for the piece, the agent creates one using the content title as the campaign name. If a hubspot_campaign_id is already on the Notion row, the agent updates that record instead of creating a duplicate.
  • The approved_copy field in Notion must be a rich text field, not a linked Google Doc. Writers must paste or type final approved copy into Notion before the approval status is set. Document this clearly in the team SOP.
  • The Buffer post_id returned on creation must be written back to the Notion record (buffer_post_id field) to enable future performance lookups by the Performance Sync Agent.
  • Fallback: if the Buffer API call fails, the agent must log the error to the error table in Supabase, alert the manager via Slack, and set the Notion status back to Approved (not Scheduled) so the piece is not silently dropped.
  • Confirm whether HubSpot contacts should be associated with campaign records at publish time, or whether the campaign-level log is sufficient for this team's reporting needs.
Performance Sync Agent

Runs on a recurring weekly schedule, every Monday at 07:00 workspace local time, to pull engagement metrics from Buffer for all content rows in the Notion database whose status is Scheduled or Published and whose publish_datetime falls within the preceding 30 days. For each matching record it fetches the Buffer post analytics using the stored buffer_post_id, extracts the relevant metrics, and writes the values back into the corresponding Notion content record fields. The agent does not make any publishing decisions and does not send Slack notifications unless a Buffer API error is encountered.

Trigger
Weekly time-based schedule: every Monday at 07:00 local time
Tools
Buffer (analytics read), Notion (database read and write)
Replaces steps
Step 8 (manual performance data export and entry)
Estimated build
6 hours, Simple complexity
// Input
notion.database_query.filter: status in ['Scheduled', 'Published']
notion.database_query.filter: publish_datetime >= now() - 30 days
notion.row.buffer_post_id: string     // Used to call Buffer analytics endpoint

// Output (per matching Notion row)
notion.row.impressions: number
notion.row.clicks: number
notion.row.likes: number
notion.row.shares: number
notion.row.comments: number
notion.row.reach: number              // Where available from Buffer; null if not supported
notion.row.last_synced_at: datetime   // Timestamp of the most recent successful sync

// Error path
supabase.error_log.insert: { agent: 'PerformanceSyncAgent', buffer_post_id, error_message, timestamp }
slack.dm.to: manager_slack_id         // Alert only on sync failure
  • Buffer's analytics API does not return granular engagement data for LinkedIn native posts or TikTok. At launch, reach and impressions for those channels should be flagged as 'not available' in the Notion record rather than left blank, to avoid confusion. A manual pull may still be needed for those channels post-launch.
  • The buffer_post_id field on each Notion record is the join key. If it is missing (i.e. a post was scheduled outside the automation), the agent must skip that row and log a warning, not error out.
  • Notion API rate limit is 3 requests per second. For databases with more than 40 active content rows, implement a 400ms delay between sequential write operations to avoid 429 errors.
  • The 30-day lookback window is a default. Confirm with the process owner whether a longer window (e.g. 60 days for slower channels) is needed before finalising the filter.
  • Mark this agent as Simple complexity; it has no branching logic and a single data direction (Buffer read, Notion write).
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow, Content Calendar Management automation
// ============================================================
// TRIGGER: New Notion content row created OR deadline reached
// ============================================================
notion.event_type: 'page_created' | 'property_changed'
notion.database_id: CONTENT_CALENDAR_DB_ID              // Set in credential store
notion.row.id: page_id                                   // Unique Notion page ID, carried throughout
notion.row.title: string                                  // e.g. '5 Tips for Q2 LinkedIn Content'
notion.row.topic: string
notion.row.format: string                                 // 'Blog post' | 'LinkedIn carousel' | etc.
notion.row.channel: string                                // 'LinkedIn' | 'Instagram' | 'Blog' | etc.
notion.row.assigned_writer: notion_person_id             // Resolved to slack_user_id via lookup
notion.row.assigned_reviewer: notion_person_id           // Resolved to slack_user_id via lookup
notion.row.deadline: ISO8601_date
notion.row.draft_link: null                              // Null at creation
notion.row.status: 'Briefed'                             // Set by agent after brief dispatch
notion.row.brief_sent: false                             // Boolean flag; prevents duplicate briefs

// ============================================================
// AGENT 1: Calendar Orchestrator Agent
// Handles: brief dispatch and overdue reminder
// ============================================================

// -- Path A: New row -> Brief dispatch
lookup(notion_person_id) -> slack_user_id                // Static map in credential store
compose(brief_message) {
  title:    notion.row.title
  topic:    notion.row.topic
  format:   notion.row.format
  channel:  notion.row.channel
  deadline: notion.row.deadline
  notion_link: 'https://notion.so/' + page_id
}
slack.chat.postMessage(
  channel: writer_slack_user_id,
  text: brief_message
)
notion.pages.update(page_id, { brief_sent: true, status: 'Briefed' })

// -- Path B: Deadline reached, draft_link still null -> Overdue reminder
schedule_check: deadline_date at 09:00 local_time
if (notion.row.draft_link == null) {
  slack.chat.postMessage(channel: writer_slack_user_id, text: overdue_reminder_1)
  slack.chat.postMessage(channel: manager_slack_user_id, text: overdue_copy_1)
  // 24 hours later, if still null:
  slack.chat.postMessage(channel: writer_slack_user_id, text: overdue_reminder_2)
}

// ============================================================
// AGENT 2: Review and Publish Agent
// Handles: review routing, Buffer scheduling, HubSpot logging
// ============================================================

// -- Trigger condition A: draft_link populated on Notion row
notion.row.draft_link: url                               // Writer logs Google Docs URL in Notion
notion.row.status: 'Draft Submitted'                     // Set by agent on detection
slack.chat.postMessage(
  channel: reviewer_slack_user_id,
  text: review_request {
    title:      notion.row.title,
    draft_link: notion.row.draft_link,
    notion_link: 'https://notion.so/' + page_id
  }
)

// -- Trigger condition B: status changes to 'Approved'
notion.row.status: 'Approved'
notion.row.approved_copy: string                         // Final post text in Notion rich text field
notion.row.publish_datetime: ISO8601_datetime
notion.row.channel -> buffer_profile_id                  // Resolved from static channel->profile map

buffer.updates.create(
  profile_ids: [buffer_profile_id],
  text:         notion.row.approved_copy,
  scheduled_at: notion.row.publish_datetime              // UTC ISO8601
) -> buffer_response.id                                  // Stored as notion.row.buffer_post_id

notion.pages.update(page_id, {
  status:         'Scheduled',
  buffer_post_id: buffer_response.id
})

hubspot.campaigns.create_or_update({
  campaign_id:   notion.row.hubspot_campaign_id | null,  // Create if null
  name:          notion.row.title,
  publish_date:  notion.row.publish_datetime,
  channel:       notion.row.channel,
  notion_url:    'https://notion.so/' + page_id
}) -> hubspot_campaign_id
notion.pages.update(page_id, { hubspot_campaign_id: hubspot_campaign_id })

// -- Trigger condition C: status changes to 'Needs Revision'
notion.row.status: 'Needs Revision'
notion.row.reviewer_comment: string                      // Free text field on Notion record
slack.chat.postMessage(
  channel: writer_slack_user_id,
  text: revision_notice {
    title:   notion.row.title,
    comment: notion.row.reviewer_comment,
    notion_link: 'https://notion.so/' + page_id
  }
)
notion.pages.update(page_id, { status: 'In Revision' })

// ============================================================
// AGENT 3: Performance Sync Agent
// Handles: weekly Buffer analytics pull and Notion write-back
// ============================================================

// -- Schedule: Every Monday 07:00 local time
notion.databases.query(CONTENT_CALENDAR_DB_ID, {
  filter: {
    and: [
      { property: 'status', select: { is_one_of: ['Scheduled', 'Published'] } },
      { property: 'publish_datetime', date: { past_month: {} } }
    ]
  }
}) -> [page_id, buffer_post_id, ...]

for each row in query_results {
  buffer.analytics.post(buffer_post_id) -> {
    impressions: number,
    clicks:      number,
    likes:       number,
    shares:      number,
    comments:    number,
    reach:       number | null
  }
  notion.pages.update(page_id, {
    impressions:     buffer_analytics.impressions,
    clicks:          buffer_analytics.clicks,
    likes:           buffer_analytics.likes,
    shares:          buffer_analytics.shares,
    comments:        buffer_analytics.comments,
    reach:           buffer_analytics.reach,
    last_synced_at:  now()
  })
  delay(400ms)                                           // Respect Notion 3 req/sec rate limit
}

// ============================================================
// ERROR PATH (all agents)
// ============================================================
on_error {
  supabase.from('automation_errors').insert({
    agent:       agent_name,
    notion_page: page_id | null,
    error_msg:   error.message,
    timestamp:   now()
  })
  slack.chat.postMessage(channel: manager_slack_user_id, text: error_alert)
}
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
One workflow per agent (three workflows total) on the chosen automation platform. Each workflow is self-contained with its own trigger and error handler. A shared credential store holds all API keys, Slack user ID maps, Buffer profile ID maps, and Notion database IDs so credentials are never hardcoded in workflow nodes. Credentials are referenced by name (e.g. NOTION_API_KEY, BUFFER_ACCESS_TOKEN, HUBSPOT_API_KEY, SLACK_BOT_TOKEN).
Webhook configuration
Calendar Orchestrator Agent: Notion webhook or polling interval of 5 minutes on the content calendar database, filtered to 'page created' and 'property changed' events. Review and Publish Agent: Notion polling on draft_link and status property changes, 5-minute interval. Polling is preferred over native Notion webhooks at this stage due to reliability; switch to Notion webhooks (where supported by the platform) once stable. Performance Sync Agent: internal cron trigger, no inbound webhook required.
Templating approach
Slack messages are composed using block kit JSON templates stored as static text assets within each workflow. Field values (title, deadline, draft_link, reviewer_comment) are injected at runtime using the platform's expression syntax. Maintain one template file per message type: brief, overdue_reminder, review_request, revision_notice, error_alert. Do not hardcode message copy inside workflow nodes.
Error logging
All agent errors are written to a Supabase table named 'automation_errors' with columns: id (uuid), agent (text), notion_page_id (text), error_message (text), created_at (timestamptz). A Slack alert is sent to the marketing manager's DM on every error insertion. The Supabase project is created and owned by FullSpec for the duration of the build; access credentials are handed over to the process owner at go-live.
Testing approach
All agents are built and tested against a sandbox Notion database (a duplicate of the production database with test content rows) before any connection to live Buffer profiles, live HubSpot campaigns, or the team's real Slack workspace. Buffer sandbox mode is used for post creation tests. Once all agents pass sandbox testing, a controlled end-to-end test runs on the live environment using one real content piece from brief to published status. See the companion Test and QA Plan (FS-DOC-06) for full test case list.
Estimated total build time
Calendar Orchestrator Agent: 10 hours. Review and Publish Agent: 12 hours. Performance Sync Agent: 6 hours. End-to-end integration testing and QA: 8 hours (included in the 28-hour total build effort from the project snapshot). Total: 36 hours gross; 28 hours net build after accounting for discovery and Notion audit work completed in week 1. Build cost: $3,200 (one-off, Standard build).
Pre-build blockers to resolve before week 1 build kickoff: (1) Notion database field names must match the schema in this document exactly, confirmed by the Notion audit. (2) Buffer plan tier must be confirmed as Essentials or above. (3) HubSpot Marketing Hub Starter or above must be confirmed for Campaigns API access. (4) Slack bot app must be created in the workspace and granted chat:write and users:read scopes. (5) The Notion-to-Slack user ID lookup table must be populated for all team members listed as assignees or reviewers. None of the three agents can be mapped until these items are resolved. Contact support@gofullspec.com to flag any blockers.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Content Calendar Management.

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