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
Social Media Scheduling
[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Social Media Scheduling automation. It covers the full current-state process map, all three agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. The FullSpec team owns everything in this document from first commit through to handover. The process owner and social media coordinator are responsible for confirming Airtable schema, supplying API credentials, and approving test content runs before go-live.
01Process snapshot
02Agent specifications
Monitors the Airtable content calendar for records whose status field transitions to 'Ready for Review'. On detection, it reads the caption text field and the Google Drive asset link field from the same record, retrieves the referenced file metadata from Google Drive to confirm the asset is accessible, and assembles a structured content package object containing all fields required by the Approval Routing Agent. If the Drive file is missing or inaccessible, the agent sets the Airtable record status to 'Asset Error' and halts the run for that record without proceeding downstream.
// Input airtable.record_id : string // Airtable record unique ID airtable.post_name : string // Human-readable post title airtable.caption_text : string // Full caption body, pre-formatted airtable.target_channels : string[] // e.g. ['instagram','facebook','linkedin'] airtable.scheduled_time : ISO8601 // Desired publish timestamp airtable.drive_asset_url : string // Google Drive file share link // Output content_package.record_id : string content_package.post_name : string content_package.caption_text : string content_package.target_channels : string[] content_package.scheduled_time : ISO8601 content_package.asset_url : string // Confirmed accessible Drive URL content_package.assembled_at : ISO8601 // Timestamp of package creation // On error (asset not found) airtable.status -> 'Asset Error' airtable.error_note -> 'Drive file unreachable at [timestamp]'
- Airtable plan must be on the Team tier or above to enable webhook automations. The Free and Plus tiers do not support outbound webhooks. Confirm the plan before build starts.
- The Airtable base field for status must use a Single Select field type named exactly 'Status'. Any variation in field name requires a config update in the trigger node.
- Google Drive retrieval uses a service account with read-only access scoped to the specific shared folder. OAuth scopes required: drive.readonly. The service account JSON key must be stored in the shared credential store, not hardcoded.
- The agent must deduplicate on record_id. If the same record fires the trigger twice (e.g. status toggled back and forth), the second run must be suppressed using a processed_ids log in the automation platform's data store.
- Confirm the Drive asset field always contains a direct file link (not a folder link) before build. A folder link cannot be resolved to a single asset without additional logic.
- Image resizing is out of scope for this build. The coordinator must upload correctly sized assets to Drive before setting status to 'Ready for Review'.
Receives the content package from the Content Intake Agent and posts a structured interactive message to the designated Slack approvals channel. The message includes the post name, formatted caption, target channels, scheduled publish time, and the Google Drive asset URL as a linked thumbnail. The message presents two interactive button actions: Approve and Reject. On Approve, the agent updates the Airtable record status to 'Approved' and passes the content package to the Publishing and Reporting Agent. On Reject, it captures the reviewer's comment from the Slack modal, writes the feedback as a note to the Airtable record, and sets the record status to 'In Revision'. If no response is received within the configured timeout window (default 48 hours), the agent sends a reminder to the coordinator and leaves the record in 'Pending Approval' status.
// Input (from Content Intake Agent) content_package.record_id : string content_package.post_name : string content_package.caption_text : string content_package.target_channels : string[] content_package.scheduled_time : ISO8601 content_package.asset_url : string // Output (on Approve) approved_package.record_id : string approved_package.caption_text : string approved_package.target_channels: string[] approved_package.scheduled_time : ISO8601 approved_package.asset_url : string approved_package.approved_by : string // Slack user display name approved_package.approved_at : ISO8601 airtable.status -> 'Approved' // Output (on Reject) airtable.status -> 'In Revision' airtable.reviewer_notes -> string // Feedback text from Slack modal airtable.rejected_at -> ISO8601 // On approval (downstream trigger for Publishing Agent) approved_package -> Publishing and Reporting Agent input
- The Slack app must have the following OAuth scopes enabled: chat:write, chat:write.public, channels:read, reactions:read, and interactivity must be switched on in the Slack app manifest. Confirm the workspace admin has approved the app before build begins.
- Interactive message buttons require the Slack app's Request URL (interactivity endpoint) to be a publicly reachable HTTPS URL. The automation platform's webhook endpoint URL must be registered here during setup.
- If the business is on Slack's free plan, message history is limited to 90 days. This may affect the approval audit trail for older records. Recommend upgrading to Pro or logging all decisions to Airtable as the primary audit record (which this agent does by design).
- The rejection modal must use a Slack Block Kit plain_text_input element to capture reviewer feedback. Do not rely on Slack thread replies, as those cannot be reliably intercepted by a webhook listener.
- The 48-hour timeout is a configurable environment variable (APPROVAL_TIMEOUT_HOURS). Set this in the credential/config store so it can be changed without a code deployment.
- Dedupe logic: if the approver presses Approve more than once (e.g. double-click), the second event must be discarded. Check approved_package.record_id against a short-lived processed set before triggering the publishing agent.
Receives an approved content package and submits it to Buffer via the Buffer API, setting the caption, asset URL, target channels, and scheduled publish time exactly as specified. After a successful Buffer API response, the agent writes the returned Buffer post ID and the confirmed publish URL back to the Airtable record and updates the record status to 'Scheduled'. Once per week, on a configurable schedule (default: Monday 08:00 in the workspace timezone), the agent queries Airtable for all records with status 'Scheduled' in the coming 7-day window and posts a formatted summary message to the designated Slack marketing channel listing each post's name, channel targets, and scheduled time with a link to the Buffer queue.
// Input (from Approval Routing Agent) approved_package.record_id : string approved_package.caption_text : string approved_package.target_channels : string[] // Must match Buffer profile IDs approved_package.scheduled_time : ISO8601 approved_package.asset_url : string approved_package.approved_by : string // Output (on successful Buffer schedule) airtable.status -> 'Scheduled' airtable.buffer_post_id -> string // Buffer-returned post ID airtable.buffer_post_url -> string // Direct link to Buffer queue entry airtable.scheduled_confirmed_at -> ISO8601 // Output (on Buffer API error) airtable.status -> 'Publish Error' airtable.error_note -> string // Buffer error message + HTTP status slack.alert_channel -> string // Error notification to coordinator // Output (weekly summary, cron trigger) slack.weekly_summary_message : Block Kit message listing all 'Scheduled' posts // Fields per post in summary: // post_name, target_channels[], scheduled_time, buffer_post_url
- Buffer API authentication uses OAuth 2.0. The access token and refresh token must be stored in the shared credential store. Confirm that the Buffer account has the Essentials plan or above, as the free plan does not expose API scheduling endpoints.
- LinkedIn and TikTok require additional platform-level OAuth authentication within Buffer beyond the standard Buffer API token. These must be confirmed and connected in the Buffer dashboard before build begins. If either platform is not yet connected in Buffer, posting to it will fail silently unless the agent validates the profile ID list at runtime.
- The target_channels field in Airtable must store Buffer profile IDs, not human-readable platform names. A reference lookup table mapping platform names to Buffer profile IDs must be created and stored as a config object in the automation platform during the Discovery stage.
- The weekly summary cron expression is configurable: SUMMARY_CRON_SCHEDULE environment variable, default '0 8 * * 1' (Monday 08:00). The timezone must be set explicitly to match the workspace locale.
- On a Buffer API 429 (rate limit) response, implement exponential backoff with a maximum of 3 retries before setting the Airtable record to 'Publish Error' and alerting the coordinator in Slack.
- The Buffer API image upload requires the asset to be a publicly accessible URL. Google Drive share links with restricted access will fail. The Content Intake Agent must verify the sharing permission is set to 'Anyone with the link can view' before assembling the content package.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────
// TRIGGER
// ─────────────────────────────────────────────────────────────
SOURCE: Airtable webhook
event.type = 'record.updated'
event.base_id = '<airtable_base_id>'
event.table_id = '<content_calendar_table_id>'
event.record_id = 'rec_XXXXXXXXXXXXXX'
event.changed_fields = { Status: 'Ready for Review' }
// ─────────────────────────────────────────────────────────────
// AGENT 1: Content Intake Agent
// ─────────────────────────────────────────────────────────────
STEP 1.1 Airtable GET record by event.record_id
-> record.fields.post_name : string
-> record.fields.caption_text : string
-> record.fields.target_channels : string[] // Buffer profile IDs
-> record.fields.scheduled_time : ISO8601
-> record.fields.drive_asset_url : string
STEP 1.2 Google Drive GET file metadata by drive_asset_url
-> drive.file.id : string
-> drive.file.webContentLink : string // Public download URL
-> drive.file.permissionIds : string[] // Verify public access
STEP 1.3 Dedupe check: record_id NOT IN processed_ids_store
IF duplicate -> halt, no downstream action
IF asset inaccessible -> Airtable PATCH status = 'Asset Error', halt
STEP 1.4 Assemble content_package
content_package = {
record_id : event.record_id,
post_name : record.fields.post_name,
caption_text : record.fields.caption_text,
target_channels : record.fields.target_channels,
scheduled_time : record.fields.scheduled_time,
asset_url : drive.file.webContentLink,
assembled_at : now()
}
-> Append record_id to processed_ids_store
// ─────────────────────────────────────────────────────────────
// HANDOFF 1: Content Intake Agent -> Approval Routing Agent
// Payload: content_package (all fields above)
// ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────
// AGENT 2: Approval Routing Agent
// ─────────────────────────────────────────────────────────────
STEP 2.1 Slack POST chat.postMessage to #approvals channel
Block Kit message includes:
-> content_package.post_name
-> content_package.caption_text
-> content_package.target_channels (formatted list)
-> content_package.scheduled_time (formatted datetime)
-> content_package.asset_url (linked thumbnail)
-> action_id: 'approve_post' button
-> action_id: 'reject_post' button (opens modal)
Slack returns:
-> slack.message_ts : string // Message timestamp / ID
Airtable PATCH record.fields.status = 'Pending Approval'
STEP 2.2a ON Slack interactive callback action_id = 'approve_post'
-> slack.user.display_name : string
-> slack.action.action_ts : ISO8601
Dedupe: IF record_id already in approved_ids_store -> discard
Airtable PATCH:
record.fields.status = 'Approved'
record.fields.approved_by = slack.user.display_name
record.fields.approved_at = slack.action.action_ts
Assemble approved_package (content_package + approved_by + approved_at)
-> HANDOFF 2
STEP 2.2b ON Slack interactive callback action_id = 'reject_post'
-> slack.modal.reviewer_notes : string // Plain text input from modal
Airtable PATCH:
record.fields.status = 'In Revision'
record.fields.reviewer_notes = slack.modal.reviewer_notes
record.fields.rejected_at = now()
-> HALT (coordinator manually revises and resets status to 'Ready for Review')
STEP 2.3 TIMEOUT: IF no callback within APPROVAL_TIMEOUT_HOURS (default 48)
-> Slack POST reminder to coordinator Slack DM
-> Airtable record.fields.status remains 'Pending Approval'
// ─────────────────────────────────────────────────────────────
// HANDOFF 2: Approval Routing Agent -> Publishing and Reporting Agent
// Payload: approved_package
// { record_id, post_name, caption_text, target_channels,
// scheduled_time, asset_url, approved_by, approved_at }
// ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────
// AGENT 3: Publishing and Reporting Agent
// ─────────────────────────────────────────────────────────────
STEP 3.1 FOR EACH channel_id IN approved_package.target_channels
Buffer POST /1/updates/create.json
body: {
text : approved_package.caption_text,
media.link : approved_package.asset_url,
profile_ids : [channel_id],
scheduled_at : approved_package.scheduled_time
}
Response:
-> buffer.update.id : string // Buffer post ID
-> buffer.update.status_url : string // Direct Buffer queue link
ON 429: exponential backoff, max 3 retries
ON persistent failure:
Airtable PATCH status = 'Publish Error', error_note = buffer.error
Slack POST alert to coordinator
STEP 3.2 ON all channels successfully scheduled
Airtable PATCH:
record.fields.status = 'Scheduled'
record.fields.buffer_post_id = buffer.update.id (last channel)
record.fields.buffer_post_url = buffer.update.status_url
record.fields.scheduled_confirmed_at = now()
STEP 3.3 CRON: SUMMARY_CRON_SCHEDULE (default '0 8 * * 1')
Airtable LIST records WHERE status = 'Scheduled'
AND scheduled_time BETWEEN now() AND now() + 7 days
Build Slack Block Kit weekly summary:
FOR EACH record: post_name, target_channels, scheduled_time, buffer_post_url
Slack POST to #marketing channel
// ─────────────────────────────────────────────────────────────
// END STATE
// Airtable record.fields.status = 'Scheduled'
// Buffer queue contains post for each target channel
// Slack #marketing has received weekly summary
// ─────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Social Media Scheduling.