Back to SEO Content Production Workflow

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

SEO Content Production Workflow

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

This document gives the FullSpec build team everything needed to construct, connect, and deploy the three automation agents that replace the most time-intensive manual steps in the SEO content production process. It covers the current-state step map, full agent specifications with IO contracts, an end-to-end data flow trace, and the recommended build stack. You and your team retain editorial review and final publish approval at all times. FullSpec handles all build, integration, and testing work end to end. Questions during build should be directed to support@gofullspec.com.

01Process snapshot

Step
Name
Description
1
Keyword Research and Topic Selection
SEO Lead pulls keyword data from Ahrefs, filters by difficulty and volume, and shortlists topics. Results are copied into Notion. Time cost: 60 min/article.
2
Topic Approval and Calendar Slot
Content Manager reviews the shortlist, approves topics, and assigns each a target publish date in Notion. Time cost: 20 min/article.
3
Brief Creation
SEO Lead builds a content brief from scratch or a loose template covering keyword, intent, structure, and competitors. Quality varies per author. Time cost: 45 min/article. BOTTLENECK.
4
Writer Assignment and Brief Handoff
Content Manager assigns article to a writer via Slack with a brief link. Writer must confirm receipt individually. Time cost: 15 min/article.
5
Draft Submission by Writer
Writer completes draft in Google Docs and notifies content manager via Slack. No automated quality check before handoff. Time cost: 180 min/article.
6
Editorial Review
Content Manager reads the draft, leaves comments, and returns it for revisions. Round-trips can happen two or three times, adding several days to the timeline. Time cost: 40 min/article. BOTTLENECK.
7
SEO Optimisation Check
After editing, SEO Lead runs draft through Surfer SEO, reviews score, and manually adjusts keyword density and headings. Time cost: 30 min/article.
8
Image Sourcing and Alt Text
Team member finds or commissions images, renames files, and writes alt text before upload. Time cost: 20 min/article.
9
CMS Publishing Setup
Content Manager copies final draft into WordPress, applies formatting, sets meta title and description, selects categories and tags, adds internal links, and sets featured image. Time cost: 35 min/article. BOTTLENECK.
10
Final Pre-Publish Review
Content Manager previews the post in WordPress, checks formatting on mobile and desktop, and confirms all metadata is correct. Time cost: 15 min/article.
11
Status Update in Content Tracker
Content Manager manually updates article status in Notion to 'Published', adds the live URL, and marks the publish date. Time cost: 10 min/article.
Time cost summary: Total manual time per article cycle is 470 minutes (approx. 7.8 hours). At 12 to 20 articles/month the process consumes approximately 7 hours of staff time every week. The automation agents replace steps 3, 4, 7, 9, 10, and 11 in full, and contribute to reducing the overhead of step 6 by delivering the SEO scorecard before the editorial pass begins. Steps 1, 2, 5, 6, and 8 remain fully or partially manual by design.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Brief Generation Agent

Triggered by a Notion status change to 'Approved', this agent pulls the target keyword from the Notion article row, queries the Ahrefs API for keyword difficulty, monthly search volume, SERP competitor URLs, and top-ranking page titles, then populates a standardised Google Docs brief template. Output fields include target keyword, secondary keywords, intent classification, recommended H2 and H3 structure, suggested word count, and up to five competitor reference URLs. The completed brief is linked back to the Notion row and the status is advanced to 'Brief Ready'. Estimated build time: 18 hours. Complexity: Moderate.

Trigger
Notion database row status changes to 'Approved'. The automation platform polls the Notion API every 5 minutes or uses a Notion webhook if available on the workspace plan.
Tools
Ahrefs (Keywords Explorer API), Google Docs (REST API, template copy and field injection), Notion (Database API, row read and patch)
Replaces steps
Step 3 (Brief Creation, 45 min) and Step 4 (Writer Assignment and Brief Handoff, 15 min)
Estimated build
18 hours, Moderate complexity
// Input
notion.row.article_id          STRING  -- unique Notion page ID
notion.row.target_keyword      STRING  -- primary keyword confirmed at approval
notion.row.assigned_writer     STRING  -- Notion user ID or Slack handle
notion.row.target_publish_date DATE    -- ISO 8601

// Ahrefs API call (Keywords Explorer v3)
GET /v3/keywords-explorer/overview
  params: keyword=<target_keyword>, country=us
  returns: difficulty, volume, global_volume, cpc
GET /v3/keywords-explorer/serp-overview
  params: keyword=<target_keyword>, country=us, limit=5
  returns: serp[].url, serp[].title, serp[].word_count

// Output
google_docs.brief.target_keyword        STRING
google_docs.brief.secondary_keywords    ARRAY[STRING]  -- derived from SERP overlap
google_docs.brief.intent_classification STRING  -- informational|transactional|navigational
google_docs.brief.recommended_headings  ARRAY[STRING]  -- H2/H3 suggestions
google_docs.brief.suggested_word_count  INTEGER
google_docs.brief.competitor_urls       ARRAY[STRING]  -- top 5 SERP URLs
google_docs.brief.doc_url               STRING  -- shareable Google Docs link

notion.row.brief_url     STRING  -- written back to Notion
notion.row.status        STRING  -- patched to 'Brief Ready'
slack.message.channel    STRING  -- assigned_writer DM or writer channel
slack.message.body       STRING  -- brief link, word count, due date, confirm prompt
  • Ahrefs API access requires a plan that includes API units. Confirm the team's current Ahrefs subscription tier before build. The Keywords Explorer v3 endpoint costs API units per request; set a monthly cap in the orchestration layer to prevent quota overruns at high article volumes (12 to 20 articles/month requires a minimum of 40 API calls/month accounting for retries).
  • Google Docs brief template must be created in the [YourCompany.com] Google Workspace account before build begins. The agent copies the master template and injects fields by placeholder tag (e.g. {{TARGET_KEYWORD}}, {{COMPETITOR_URL_1}}). Confirm placeholder naming convention with the content team prior to template creation.
  • Dedupe: if a Notion row is accidentally moved back to 'Approved' after a brief already exists, the agent must check for a non-empty brief_url field and skip re-generation, logging a warning instead of creating a duplicate document.
  • Fallback: if the Ahrefs API returns an error or zero results for a keyword, the agent must post a Slack alert to the content manager with the article ID and keyword, set the Notion status back to 'Needs Review', and halt without creating a partial brief.
  • Notion polling interval must not exceed 5 minutes. If the workspace supports webhooks via the Notion API (available on Business plan and above), switch to event-driven triggering to reduce latency and API call overhead.
  • Must confirm before build: Notion field schema (field names, exact status label strings), Google Docs template ID, Ahrefs API key with confirmed unit quota, Slack workspace token and target channel or DM routing logic.
SEO Audit Agent

Triggered when a writer marks the Notion article row as 'Draft Ready', this agent retrieves the current Google Docs draft content, exports it as plain text, and submits it to the Surfer SEO Content Editor API for scoring against the target keyword. It retrieves the overall content score and a section-level recommendation list, identifies any headings or paragraphs scoring below the configured threshold (default: 67), and packages the results into a structured Slack scorecard message delivered to the content manager. The content score and a link to the Surfer SEO report are also written back to the Notion article row. Estimated build time: 16 hours. Complexity: Moderate.

Trigger
Notion database row status changes to 'Draft Ready'. Same polling or webhook mechanism as Agent 1.
Tools
Surfer SEO (Content Editor API), Google Docs (export as plain text), Slack (incoming webhook or Bot API), Notion (Database API, row read and patch)
Replaces steps
Step 7 (SEO Optimisation Check, 30 min) and Step 11 (Status Update in Content Tracker, 10 min, partial)
Estimated build
16 hours, Moderate complexity
// Input
notion.row.article_id          STRING
notion.row.target_keyword      STRING
notion.row.google_docs_draft   STRING  -- Google Docs document ID
notion.row.assigned_editor     STRING  -- Slack handle for content manager
notion.row.surfer_threshold    INTEGER -- default 67, configurable per project

// Google Docs export
GET /v1/documents/{documentId}/export?mimeType=text/plain
  returns: plain_text_content STRING

// Surfer SEO API call (Content Editor)
POST /v1/content-editor/analyze
  body: { keyword: target_keyword, content: plain_text_content, locale: 'en-US' }
  returns: content_score INTEGER, sections[].heading STRING,
           sections[].score INTEGER, sections[].recommendations ARRAY[STRING]

// Output
slack.scorecard.content_score      INTEGER
slack.scorecard.flagged_sections   ARRAY[{ heading STRING, score INTEGER, notes STRING }]
slack.scorecard.draft_link         STRING  -- Google Docs URL
slack.scorecard.surfer_report_url  STRING  -- direct Surfer SEO report link
slack.scorecard.below_threshold    BOOLEAN -- true if content_score < surfer_threshold

notion.row.seo_score             INTEGER -- written back
notion.row.surfer_report_url     STRING  -- written back
notion.row.status                STRING  -- patched to 'In Review'
  • Surfer SEO API access must be confirmed on the team's current plan. Not all Surfer SEO tiers expose the Content Editor API. Confirm API availability and monthly analysis credit limit before build begins.
  • Google Docs export: the draft document must be shared with the service account or OAuth client used by the automation platform. Confirm sharing permissions before integration test.
  • Score threshold of 67 is the default. This value must be stored as a configurable environment variable in the orchestration layer, not hardcoded, so the content manager can adjust it without a code change.
  • Dedupe: if the Notion row is set to 'Draft Ready' more than once (e.g. after revisions), the agent must re-run and overwrite the previous Surfer score in Notion rather than creating a duplicate Slack message chain. Use the article_id as the idempotency key.
  • Fallback: if the Surfer SEO API is unavailable or returns an error, send a Slack alert to the content manager stating the audit failed, preserve the 'Draft Ready' status in Notion, and log the error to the central error table. Do not silently skip.
  • Must confirm before build: Surfer SEO API key, plan tier and credit limit, score threshold agreed with SEO Lead, Slack bot token and channel ID for editor notifications, Google Docs service account OAuth scope (drive.readonly or drive.file).
CMS Publishing Agent

Triggered when the content manager marks the Notion article row 'Approved for Publish', this agent reads the final approved draft from Google Docs and the full set of metadata fields from the Notion record, then creates a WordPress post via the WordPress REST API. It maps Notion fields directly to WordPress post parameters: meta title, meta description, slug, categories, tags, featured image URL, and scheduled publish date. After WordPress confirms the post is created or scheduled, the agent writes the canonical URL back to the Notion row and updates the status to 'Published'. This is the only agent that writes to the production CMS and must be tested exclusively against a WordPress staging environment before go-live. Estimated build time: 20 hours. Complexity: Complex.

Trigger
Notion database row status changes to 'Approved for Publish'. The content manager must set this status manually. No automated promotion to this status is permitted.
Tools
WordPress (REST API v2, posts and media endpoints), Google Docs (export as HTML), Notion (Database API, row read and patch)
Replaces steps
Step 9 (CMS Publishing Setup, 35 min), Step 10 (Final Pre-Publish Review, 15 min, partial), and Step 11 (Status Update in Content Tracker, 10 min)
Estimated build
20 hours, Complex complexity
// Input
notion.row.article_id              STRING
notion.row.google_docs_final       STRING  -- Google Docs document ID (approved draft)
notion.row.meta_title              STRING  -- max 60 chars
notion.row.meta_description        STRING  -- max 160 chars
notion.row.slug                    STRING  -- URL-safe, lowercase, hyphenated
notion.row.wp_categories           ARRAY[STRING]  -- must match existing WP category names
notion.row.wp_tags                 ARRAY[STRING]
notion.row.featured_image_url      STRING  -- publicly accessible URL
notion.row.publish_date            DATETIME -- ISO 8601, used for scheduling
notion.row.author_id               INTEGER  -- WordPress user ID

// Google Docs export
GET /v1/documents/{documentId}/export?mimeType=text/html
  returns: html_content STRING  -- cleaned before POST to WordPress

// WordPress REST API calls
POST /wp/v2/media
  body: { source_url: featured_image_url }  -- sideload featured image
  returns: media_id INTEGER

POST /wp/v2/posts
  body: {
    title:          meta_title,
    content:        html_content,
    slug:           slug,
    status:         'future' | 'publish',
    date:           publish_date,
    categories:     [category_id, ...],
    tags:           [tag_id, ...],
    featured_media: media_id,
    author:         author_id,
    meta: {
      _yoast_wpseo_title:    meta_title,
      _yoast_wpseo_metadesc: meta_description
    }
  }
  returns: post_id INTEGER, link STRING (canonical URL)

// Output
notion.row.canonical_url   STRING  -- WordPress post permalink
notion.row.wp_post_id      INTEGER -- for future reference
notion.row.status          STRING  -- patched to 'Published'
notion.row.published_date  DATE    -- actual or scheduled date confirmed by WP
  • WordPress REST API authentication must use Application Passwords (WordPress 5.6+) or OAuth 2.0 via the WP OAuth Server plugin. Do not use admin credentials directly. Create a dedicated WordPress user with Editor role and generate an application password scoped to that account only.
  • Category and tag resolution: Notion stores category and tag names as strings. The agent must resolve these to WordPress term IDs before the POST request using GET /wp/v2/categories?search={name} and GET /wp/v2/tags?search={name}. If a term does not exist, the agent must either create it (confirm this behaviour with the content team) or halt and alert via Slack.
  • HTML cleaning: Google Docs HTML export includes inline styles and Google-specific span wrappers. The agent must strip all inline styles and non-semantic spans before posting to WordPress to avoid visual inconsistencies in the CMS theme.
  • Yoast SEO meta fields are written via the post meta API. Confirm that the Yoast SEO plugin is installed and the REST API meta fields are exposed. Alternative: if the team uses RankMath, the meta field keys differ (_rank_math_title and _rank_math_description).
  • Featured image sideloading requires the WordPress media endpoint to accept the source_url parameter. This is available in WordPress 5.8+. Confirm WordPress version before build.
  • This agent must never run against the production WordPress environment during testing. A staging environment with identical plugin configuration must be provisioned before any integration test begins.
  • Dedupe: if an article row is accidentally set to 'Approved for Publish' a second time (e.g. after a manual status correction), the agent must check for a non-empty wp_post_id field in Notion and skip creation, posting a Slack warning to the content manager instead.
  • Must confirm before build: WordPress application password credentials, staging environment URL and credentials, Yoast or RankMath plugin and meta field key names, category and tag creation policy, Google Docs OAuth scope for final draft (drive.readonly minimum).
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow across all three agents. Field names match the Notion schema and API response contracts defined in Section 02.
// ─────────────────────────────────────────────────────────────
// TRIGGER: Content Manager sets Notion row status to 'Approved'
// ─────────────────────────────────────────────────────────────
notion.database.poll OR notion.webhook.status_changed
  -> filter: row.status == 'Approved'
  -> extract: article_id, target_keyword, assigned_writer,
              target_publish_date

// ─────────────────────────────────────────────────────────────
// AGENT 1: Brief Generation Agent
// ─────────────────────────────────────────────────────────────
ahrefs.keywords_explorer.overview(
  keyword = target_keyword,
  country = 'us'
) -> keyword_difficulty, volume, cpc

ahrefs.keywords_explorer.serp_overview(
  keyword = target_keyword,
  country = 'us',
  limit   = 5
) -> serp[].url, serp[].title, serp[].word_count

google_docs.template.copy(master_template_id)
  -> new_doc_id

google_docs.document.replace_placeholders(new_doc_id, {
  TARGET_KEYWORD:      target_keyword,
  SECONDARY_KEYWORDS:  derived_from_serp_overlap,
  INTENT:              intent_classification,
  HEADINGS:            recommended_headings[],
  WORD_COUNT:          suggested_word_count,
  COMPETITOR_URL_1:    serp[0].url,
  COMPETITOR_URL_2:    serp[1].url,
  COMPETITOR_URL_3:    serp[2].url,
  COMPETITOR_URL_4:    serp[3].url,
  COMPETITOR_URL_5:    serp[4].url
})
  -> brief_doc_url

notion.row.patch(article_id, {
  brief_url: brief_doc_url,
  status:    'Brief Ready'
})

slack.send_dm(assigned_writer, {
  text: 'Brief ready for [target_keyword]. Link: [brief_doc_url]',
  word_count: suggested_word_count,
  due_date:   target_publish_date,
  action:     'Please confirm receipt and mark Notion row Draft Ready when done.'
})

// ─────────────────────────────────────────────────────────────
// HANDOFF: Writer completes draft in Google Docs.
//          Writer sets Notion row status to 'Draft Ready'.
// ─────────────────────────────────────────────────────────────
notion.database.poll OR notion.webhook.status_changed
  -> filter: row.status == 'Draft Ready'
  -> extract: article_id, target_keyword, google_docs_draft,
              assigned_editor, surfer_threshold

// ─────────────────────────────────────────────────────────────
// AGENT 2: SEO Audit Agent
// ─────────────────────────────────────────────────────────────
google_docs.export(google_docs_draft, mime_type='text/plain')
  -> plain_text_content

surfer_seo.content_editor.analyze({
  keyword: target_keyword,
  content: plain_text_content,
  locale:  'en-US'
})
  -> content_score, sections[].heading, sections[].score,
     sections[].recommendations[], surfer_report_url

// Flag sections below threshold
flagged_sections = sections[].filter(s => s.score < surfer_threshold)

slack.send_message(assigned_editor_channel, {
  content_score:     content_score,
  below_threshold:   content_score < surfer_threshold,
  flagged_sections:  flagged_sections[].{ heading, score, notes },
  draft_link:        google_docs_draft_url,
  surfer_report_url: surfer_report_url
})

notion.row.patch(article_id, {
  seo_score:        content_score,
  surfer_report_url: surfer_report_url,
  status:           'In Review'
})

// ─────────────────────────────────────────────────────────────
// HANDOFF: Content Manager reads draft and SEO scorecard.
//          Content Manager sets Notion row to 'Approved for Publish'.
//          THIS IS A DELIBERATE HUMAN CHECKPOINT. NO AUTOMATION
//          MAY ADVANCE THIS STATUS WITHOUT CONTENT MANAGER ACTION.
// ─────────────────────────────────────────────────────────────
notion.database.poll OR notion.webhook.status_changed
  -> filter: row.status == 'Approved for Publish'
  -> guard:  row.wp_post_id == null  // dedupe check
  -> extract: article_id, google_docs_final, meta_title,
              meta_description, slug, wp_categories[], wp_tags[],
              featured_image_url, publish_date, author_id

// ─────────────────────────────────────────────────────────────
// AGENT 3: CMS Publishing Agent
// ─────────────────────────────────────────────────────────────
google_docs.export(google_docs_final, mime_type='text/html')
  -> raw_html_content

html_cleaner.strip_inline_styles(raw_html_content)
  -> clean_html_content

// Resolve category and tag names to WordPress term IDs
wordpress.api.GET('/wp/v2/categories', { search: wp_categories[] })
  -> category_ids[]
wordpress.api.GET('/wp/v2/tags', { search: wp_tags[] })
  -> tag_ids[]

// Sideload featured image
wordpress.api.POST('/wp/v2/media', { source_url: featured_image_url })
  -> media_id

// Create or schedule the WordPress post
wordpress.api.POST('/wp/v2/posts', {
  title:          meta_title,
  content:        clean_html_content,
  slug:           slug,
  status:         publish_date > now() ? 'future' : 'publish',
  date:           publish_date,
  categories:     category_ids[],
  tags:           tag_ids[],
  featured_media: media_id,
  author:         author_id,
  meta: {
    _yoast_wpseo_title:    meta_title,
    _yoast_wpseo_metadesc: meta_description
  }
})
  -> post_id, canonical_url

notion.row.patch(article_id, {
  canonical_url:  canonical_url,
  wp_post_id:     post_id,
  status:         'Published',
  published_date: publish_date
})

// ─────────────────────────────────────────────────────────────
// END: Notion content calendar row reflects Published status.
//      Canonical URL available to entire content team.
// ─────────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform (tool to be confirmed at build kick-off). One dedicated workflow per agent: Brief Generation Workflow, SEO Audit Workflow, and CMS Publishing Workflow. All three workflows share a single credential store so API keys are stored once and referenced by name, not duplicated across workflows. Environment variables are used for all configurable values (score threshold, article cap, staging vs production WordPress URL).
Webhook configuration
Where the Notion workspace plan supports webhooks (Business plan or above), configure event-driven triggers on the Notion database for status field changes to 'Approved', 'Draft Ready', and 'Approved for Publish'. Where polling is required (lower-tier Notion plans), set the poll interval to 5 minutes. WordPress confirmation events are handled via the API response body; no inbound webhook from WordPress is required. Slack notifications are outbound only via the Slack Bot API using a dedicated FullSpec-created Slack app installed to the [YourCompany.com] Slack workspace.
Templating approach
Google Docs brief template: a single master document owned by the [YourCompany.com] Google Workspace service account with placeholder tags in double-brace syntax ({{FIELD_NAME}}). The automation platform copies the master template to a new document per article and performs a find-and-replace pass to inject all values. The master template is never modified by automation. WordPress post content is templated by cleaning the Google Docs HTML export; no separate HTML template is maintained. Slack scorecard messages use a structured Block Kit layout with a header, score badge, and flagged-section list.
Error logging
All agent errors (API failures, missing Notion fields, WordPress publish rejections, Surfer SEO quota exceeded) are written to a central Supabase table named content_pipeline_errors with columns: error_id (UUID), article_id (STRING), agent_name (STRING), error_code (STRING), error_message (TEXT), timestamp (TIMESTAMPTZ), and resolved (BOOLEAN). A Slack alert is sent to the designated ops channel (#content-automation-alerts) for every new error row. The content manager is also notified directly when the error relates to an article assigned to them. Resolved errors are marked via a simple Slack button interaction or manual Supabase update.
Testing approach
All agents are built and tested in sandbox environments first: Notion test database (duplicated schema), Ahrefs sandbox or low-cost test keyword, Surfer SEO staging API key if available, and the WordPress staging environment (required, not optional). Each agent is tested independently with 3 to 5 representative articles before end-to-end pipeline testing begins. End-to-end testing runs 10 articles through the full automated pipeline in parallel with the existing manual process, as specified in the delivery plan. The production WordPress environment is not touched until end-to-end testing is signed off.
Estimated total build time
Brief Generation Agent: 18 hours. SEO Audit Agent: 16 hours. CMS Publishing Agent: 20 hours. End-to-end integration testing and pipeline validation: 6 hours. Total: 60 hours. This matches the confirmed build_effort_hours in the process specification.
Before any build work begins, the FullSpec team requires the following to be confirmed and supplied: (1) Notion field schema with exact field names and status label strings for all three trigger statuses, (2) Ahrefs API key with confirmed plan tier and monthly unit quota, (3) Surfer SEO API key with confirmed plan tier and monthly analysis credit limit, (4) WordPress application password for a dedicated Editor-role user on the staging environment, (5) Google Workspace service account credentials with drive.file and docs scope, (6) Slack bot token with chat:write and channels:read scopes, (7) Google Docs master brief template ID, and (8) Supabase project URL and service role key for error logging. Missing any of these items will delay the build start. Contact support@gofullspec.com to submit credentials securely.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for SEO Content Production Workflow.

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