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
OKR & Goal Tracking Automation
[YourCompany.com] · Management Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for the FullSpec team building and deploying the OKR & Goal Tracking automation. It covers the full current-state process map with bottlenecks identified, precise specifications for each automation agent, the end-to-end data flow across all integrated tools, and the recommended build stack with estimated effort. Everything the build team needs to start, validate, and hand over the completed automation is contained here. The process owner does not need to action this document directly; it is written for the FullSpec specialist assigned to this build.
01Process snapshot
02Agent specifications
Sends structured OKR update requests to each configured team lead via Slack on the agreed cycle schedule (weekly or fortnightly). Tracks submission status per team lead in Google Sheets in real time. Fires a first follow-up reminder at 24 hours and a second at 48 hours for any team lead who has not submitted. Closes the collection window at the configured deadline and logs all responses plus a non-responder list to the master Google Sheet. Eliminates all manual outreach, chasing, and free-text transcription from the current process.
// Input
schedule_trigger: { cadence: 'weekly' | 'fortnightly', day_of_week: string, time_utc: string }
team_leads: [ { slack_user_id: string, name: string, key_results: string[] } ]
collection_window_hours: number // default 48
// Output (written to Google Sheets master OKR tab)
cycle_responses: [
{
cycle_date: string, // ISO date of cycle trigger
team_lead_id: string,
team_lead_name: string,
key_result_id: string,
key_result_label: string,
current_value: number,
target_value: number,
submitted_at: string, // ISO timestamp
submission_status: 'submitted' | 'missing'
}
]
non_responder_log: [ { slack_user_id: string, name: string, reminders_sent: number } ]
// On reminder (24 h and 48 h)
slack_reminder_payload: {
channel: slack_user_id,
text: string, // templated follow-up message
reminder_number: 1 | 2
}- Slack app tier requirement: the workspace must have a plan that supports the Slack Workflow Builder or allows incoming webhooks and bot message posting via the Slack Web API. Free Slack plans may not support interactive message components; if confirmed on free tier, fall back to a Google Form link posted via bot message and parse form responses via the Google Sheets API instead.
- The Slack app must be installed with the following OAuth scopes: chat:write, chat:write.public, users:read, users:read.email, channels:read, and im:write. Confirm these scopes are approved before build begins (workspace admin approval required per lifecycle data: Slack app permissions granted May 8).
- Team lead roster must be maintained as a named configuration table (tab: CONFIG_TeamLeads) in the master Google Sheet with columns: slack_user_id, name, active (boolean), key_result_ids (comma-separated). The agent reads this table at trigger time; adding or removing a team lead only requires a row edit.
- Dedupe logic: if a team lead submits more than once within the same cycle window, only the most recent submission (by submitted_at timestamp) is written to the active cycle row. Previous submissions are appended to a separate audit log tab (tab: AUDIT_Submissions).
- If a team lead has no Slack account or is in a different channel structure, a manual exception row must be pre-populated in the master sheet by the ops manager before the collection window opens. The agent must detect a pre-populated row and skip that user's Slack message, logging them as 'manual-entry' in submission_status.
- The collection window close time must be configurable without a code change. Store it as a named range (CONFIG.collection_window_hours) in the Google Sheet so the ops manager can update it directly.
- Confirm before build: agreed cycle cadence (weekly vs fortnightly), exact Slack channel IDs for each team lead DM, Google Sheet ID and tab names, and whether Google Workspace calendar integration is needed for the trigger or whether a fixed cron schedule is sufficient.
Reads the completed cycle response rows from the master Google Sheet once the collection window closes (or all team leads have submitted, whichever is earlier). Calculates progress percentage for each key result against its target value. Applies the configured RAG thresholds to assign a status of Green, Amber, or Red to each key result. Identifies objectives where one or more key results are Red or where the aggregate objective score falls below the at-risk threshold. Creates a new Notion page for the cycle using the standard cycle summary template. Posts a formatted digest to the designated leadership Slack channel, summarising overall OKR health, listing at-risk items, and linking to the Notion page. Archives the cycle data to a timestamped tab in the master Google Sheet.
// Input (read from Google Sheets master OKR tab on trigger)
cycle_responses: [
{
cycle_date: string,
team_lead_id: string,
key_result_id: string,
key_result_label: string,
current_value: number,
target_value: number,
submission_status: 'submitted' | 'missing'
}
]
rag_thresholds: {
green_min_pct: number, // e.g. 70 (>= 70% = Green)
amber_min_pct: number, // e.g. 40 (>= 40% = Amber, < 70%)
red_below_pct: number // e.g. 40 (< 40% = Red)
}
notion_template_id: string // ID of the cycle summary template page in Notion
leadership_slack_channel_id: string
// Output: Google Sheets (calculated fields written back to master tab)
per_key_result: {
progress_pct: number, // (current_value / target_value) * 100
rag_status: 'Green' | 'Amber' | 'Red',
at_risk_flag: boolean
}
// Output: Notion page created
notion_page: {
parent_database_id: string,
title: string, // 'OKR Cycle Summary - {cycle_date}'
properties: {
cycle_date: string,
overall_rag: 'Green' | 'Amber' | 'Red',
at_risk_count: number,
submission_rate_pct: number
},
content_blocks: [ // progress table + at-risk list + auto-commentary
'progress_table',
'at_risk_objectives_list',
'auto_generated_commentary'
]
}
// Output: Slack digest posted to leadership channel
slack_digest: {
channel: leadership_slack_channel_id,
blocks: [
{ type: 'header', text: 'OKR Cycle Summary - {cycle_date}' },
{ type: 'section', text: 'Overall health: {overall_rag}' },
{ type: 'section', text: 'Submission rate: {submission_rate_pct}%' },
{ type: 'section', text: 'At-risk objectives: {at_risk_count}' },
{ type: 'actions', elements: [ { type: 'button', text: 'View full report', url: notion_page_url } ] }
]
}
// Archive output
archived_tab_name: string // format: 'ARCHIVE_{cycle_date}' e.g. 'ARCHIVE_2025-05-19'- RAG thresholds must be agreed and signed off by the ops manager and founder before the Analysis Agent build begins. Store thresholds as named ranges (CONFIG.green_min_pct, CONFIG.amber_min_pct, CONFIG.red_below_pct) in the Google Sheet so they can be updated without a code change.
- Notion integration requires a Notion integration token with the following capabilities: read content, insert content, and update content on the OKR workspace. The integration must be explicitly shared with the parent database or page that will contain cycle summary pages. Confirm the Notion workspace tier supports API access (free Notion plans have API access; confirm no enterprise SSO restrictions apply).
- The Notion cycle summary template page must be created and its page ID confirmed before build. The agent duplicates this template page for each cycle rather than building page content from scratch, ensuring formatting consistency.
- The leadership Slack channel ID must be confirmed and the Slack bot must be added as a member of that channel before the first test run. Use the Slack API channels.info endpoint to validate the channel ID during build.
- Progress percentage calculation: progress_pct = (current_value / target_value) * 100, capped at 100% for display purposes. Key results where target_value is 0 must be flagged as a configuration error and skipped rather than producing a divide-by-zero exception.
- The auto-generated commentary in the Notion page uses a simple template approach: three fixed sections (On Track, At Risk, Missing Data) populated from the calculated RAG rows. Do not use an LLM for commentary generation unless explicitly scoped later. Keep it deterministic and auditable.
- Archive tab creation must be idempotent: if a tab with the same cycle_date name already exists (e.g. from a rerun), append a suffix (_v2) rather than overwriting. Log the action to the AUDIT_Submissions tab.
- Confirm before build: Notion integration token and parent database ID, leadership Slack channel ID, agreed RAG threshold values, and whether submission_rate_pct below a configurable threshold should block digest delivery or include a warning banner instead.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER: Scheduled cron fires on automation platform
// ─────────────────────────────────────────────────────────────────
trigger_event: {
cadence: 'weekly' | 'fortnightly',
fired_at: ISO_timestamp,
cycle_date: ISO_date // used as primary key for this cycle
}
// ────────────────────────────────────��────────────────────────────
// AGENT 1: OKR Collection Agent
// ─────────────────────────────────────────────────────────────────
// Step 1: Read team lead roster from Google Sheets
GoogleSheets.read(sheet_id, tab='CONFIG_TeamLeads') -> team_leads: [
{ slack_user_id, name, active: true, key_result_ids: string[] }
]
// Step 2: Send structured Slack message to each active team lead
for each team_lead in team_leads where active == true:
Slack.chat_postMessage({
channel: team_lead.slack_user_id,
text: 'OKR update due for cycle {cycle_date}',
blocks: [
{ type: 'input', label: 'Key result: {kr_label}',
element: { type: 'number_input', action_id: 'kr_{key_result_id}_value' } },
...(one input block per key_result_id)
{ type: 'actions', elements: [ { type: 'button', text: 'Submit', action_id: 'submit_okr' } ] }
]
})
GoogleSheets.writeRow(tab='LIVE_Cycle', {
cycle_date, team_lead_id, submission_status: 'pending',
reminder_count: 0, last_action_at: fired_at
})
// Step 3: Wait loop — poll submission status at 24 h intervals
at T+24h:
missing = GoogleSheets.query(tab='LIVE_Cycle',
filter={ cycle_date, submission_status: 'pending' })
for each m in missing:
Slack.chat_postMessage({ channel: m.slack_user_id,
text: 'Reminder 1: OKR update still needed for {cycle_date}' })
GoogleSheets.updateRow(tab='LIVE_Cycle',
{ reminder_count: 1, last_action_at: now() })
at T+48h (or collection_window_close, whichever is first):
missing = GoogleSheets.query(tab='LIVE_Cycle',
filter={ cycle_date, submission_status: 'pending' })
for each m in missing:
Slack.chat_postMessage({ channel: m.slack_user_id,
text: 'Final reminder: window closes in 1 hour for {cycle_date}' })
GoogleSheets.updateRow(tab='LIVE_Cycle',
{ reminder_count: 2, last_action_at: now() })
// Step 4: Handle Slack form submission callback
Slack.interactivity_callback -> payload: {
user: { id: slack_user_id },
actions: [ { action_id: 'submit_okr', ... } ],
state: { values: { kr_{key_result_id}_value: { value: number } } }
}
parse payload ->
for each key_result_id in team_lead.key_result_ids:
GoogleSheets.writeRow(tab='MASTER_OKR', {
cycle_date,
team_lead_id: payload.user.id,
team_lead_name: lookup(team_leads, slack_user_id).name,
key_result_id: key_result_id,
key_result_label: lookup(CONFIG_KRs, key_result_id).label,
current_value: payload.state.values[kr_{key_result_id}_value],
target_value: lookup(CONFIG_KRs, key_result_id).target_value,
submitted_at: now(),
submission_status: 'submitted'
})
GoogleSheets.updateRow(tab='LIVE_Cycle',
{ submission_status: 'submitted', submitted_at: now() })
// Step 5: Collection window close — mark remaining as 'missing'
GoogleSheets.updateRows(tab='LIVE_Cycle',
filter={ cycle_date, submission_status: 'pending' },
set={ submission_status: 'missing' })
GoogleSheets.writeTab(tab='AUDIT_NonResponders', {
cycle_date, non_responders: [ { slack_user_id, name, reminders_sent } ]
})
// ─────────────────────────────────────────────────────────────────
// AGENT HANDOFF: OKR Collection Agent -> OKR Analysis and Reporting Agent
// Condition: all LIVE_Cycle rows for cycle_date have status != 'pending'
// Handoff payload: cycle_date (string) passed to Analysis Agent trigger
// ─────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────
// AGENT 2: OKR Analysis and Reporting Agent
// ─────────────────────────────────────────────────────────────────
// Step 6: Read submitted key result values for the cycle
GoogleSheets.query(tab='MASTER_OKR',
filter={ cycle_date }) -> cycle_rows: [
{ key_result_id, key_result_label, current_value,
target_value, team_lead_name, submission_status }
]
// Step 7: Calculate progress and assign RAG status
rag_config = GoogleSheets.readNamedRanges([
'CONFIG.green_min_pct', // e.g. 70
'CONFIG.amber_min_pct', // e.g. 40
'CONFIG.red_below_pct' // e.g. 40
])
for each row in cycle_rows where submission_status == 'submitted':
if target_value == 0: flag as CONFIG_ERROR, skip
progress_pct = min((current_value / target_value) * 100, 100)
rag_status = progress_pct >= rag_config.green_min_pct ? 'Green'
: progress_pct >= rag_config.amber_min_pct ? 'Amber'
: 'Red'
at_risk_flag = rag_status == 'Red'
GoogleSheets.updateRow(tab='MASTER_OKR', {
key_result_id, cycle_date,
progress_pct, rag_status, at_risk_flag
})
// Step 8: Aggregate cycle-level summary
at_risk_krs = cycle_rows.filter(at_risk_flag == true)
submitted_count = cycle_rows.filter(submission_status == 'submitted').length
total_count = cycle_rows.length
submission_rate_pct = (submitted_count / total_count) * 100
overall_rag = at_risk_krs.length > 0 ? 'Red' : 'Green' // simplified
// Step 9: Create Notion cycle summary page
Notion.duplicatePage(template_id: notion_template_id) -> new_page_id
Notion.updatePage(new_page_id, {
title: 'OKR Cycle Summary - {cycle_date}',
properties: {
cycle_date, overall_rag,
at_risk_count: at_risk_krs.length,
submission_rate_pct
}
})
Notion.appendBlocks(new_page_id, [
progress_table_block(cycle_rows), // table: kr_label, progress_pct, rag_status, team_lead
at_risk_list_block(at_risk_krs), // bulleted list of Red key results
commentary_block(cycle_rows) // templated text: On Track / At Risk / Missing Data
])
notion_page_url = Notion.getPageUrl(new_page_id)
// Step 10: Post leadership digest to Slack
Slack.chat_postMessage({
channel: CONFIG.leadership_slack_channel_id,
blocks: [
{ type: 'header', text: 'OKR Cycle Summary - {cycle_date}' },
{ type: 'section', text: 'Overall health: {overall_rag} | Submissions: {submission_rate_pct}%' },
{ type: 'section', text: 'At-risk objectives: {at_risk_krs.length}' },
{ type: 'section', text: at_risk_krs.map(kr => '{kr_label} ({team_lead_name}): {progress_pct}%') },
{ type: 'actions', elements: [
{ type: 'button', text: 'View full Notion report', url: notion_page_url }
]}
]
})
// Step 11: Archive cycle data
archive_tab_name = 'ARCHIVE_{cycle_date}'
if GoogleSheets.tabExists(archive_tab_name):
archive_tab_name = archive_tab_name + '_v2'
GoogleSheets.duplicateTab(tab='MASTER_OKR', new_name=archive_tab_name)
GoogleSheets.appendRow(tab='AUDIT_Submissions', {
cycle_date, archive_tab_name, archived_at: now()
})
// ─────────────────────────────────────────────────────────────────
// END: Cycle complete. Human reviews at-risk flags. No auto-action taken.
// ─────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for OKR & Goal Tracking.