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
Capacity Planning Automation
[YourCompany.com] · Operations Department · Prepared by FullSpec · [Today's Date]
This document is the authoritative technical reference for building the Capacity Planning automation. It covers the 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 uses this document to guide the build from first credential to final handover. Reach out to support@gofullspec.com with any clarification requests before beginning agent construction.
01Process snapshot
02Agent specifications
This agent is the data ingestion layer for the entire automation. It triggers on a Monday morning schedule or on receipt of a HubSpot Closed Won webhook and then makes three sequential API calls: one to Asana for task allocations, one to Google Calendar for leave events, and one to HubSpot for late-stage pipeline deals. It assembles a single normalised JSON dataset that downstream agents consume. No calculations or notifications are performed here; the sole responsibility is reliable, deduplicated data retrieval from all three source systems.
// Input
trigger_type: 'schedule' | 'hubspot_webhook'
trigger_timestamp: ISO8601
week_start_date: YYYY-MM-DD // Monday of the planning week
hubspot_deal_id?: string // present only on webhook trigger
// Output
capacity_dataset: {
generated_at: ISO8601,
week_start_date: YYYY-MM-DD,
team_members: [
{
member_id: string,
display_name: string,
asana_user_id: string,
allocated_hours: number, // from Asana task assignments
leave_hours: number, // from Google Calendar events
available_hours: number, // 40 minus leave_hours
pipeline_demand_hours: number // from HubSpot deal resource field
}
],
pipeline_deals: [
{
deal_id: string,
deal_name: string,
close_date: YYYY-MM-DD,
resource_hours_estimate: number,
assigned_member_ids: string[]
}
],
data_quality_flags: string[] // e.g. 'member_x: no Asana tasks found'
}- Asana API access requires Business plan or higher. Confirm subscription tier before build. The free plan does not expose the Tasks search endpoint with assignee and due_on filters at scale.
- Use the Asana GET /tasks endpoint with query params: assignee=<user_gid>&project=<portfolio_gid>&due_on.after=<week_start>&due_on.before=<week_end>. Request fields: assignee, name, custom_fields (for hours estimate), completed.
- Google Calendar leave detection reads events from each team member's primary calendar using a service account with domain-wide delegation. Filter on event title containing keywords: 'leave', 'holiday', 'OOO', 'absent'. Duration is calculated from event start/end datetimes; all-day events count as 8 hours each.
- If leave is tracked in an HR system rather than Google Calendar, this integration point must be replaced before build begins. Do not assume Calendar is the source of truth without confirming with the operations manager.
- HubSpot pipeline pull uses the Deals API: GET /crm/v3/objects/deals with filter deal_stage IN [late_stage_ids] and close_date within next 14 days. The resource estimate field must be a populated HubSpot custom deal property (internal name: resource_hours_estimate). If this field does not exist, it must be created and back-filled before data from this step is meaningful.
- Deduplication: if both a schedule trigger and a webhook trigger fire within the same Monday, check for an existing capacity_dataset record with the same week_start_date in the error log store. Skip re-execution if a completed record exists from the last 6 hours.
- Fallback behaviour: if any single source API call fails, log the error with source name and HTTP status, populate that source's fields with null, set a data_quality_flag entry, and allow the pipeline to continue. The Capacity Analysis Agent must handle null values gracefully.
This agent receives the structured dataset from the Data Collection Agent and performs all numerical reasoning. It calculates utilisation percentage for each team member (allocated hours plus pipeline demand hours, divided by available hours), flags anyone over 100% as overloaded and anyone under 60% as underloaded, and records the cause of each flag. It then writes the completed utilisation table to Google Sheets and sets the status flag that determines whether the Reporting and Notification Agent fires a detailed alert or a routine summary.
// Input
capacity_dataset: <object> // full output from Data Collection Agent
// Output
analysis_result: {
analysed_at: ISO8601,
week_start_date: YYYY-MM-DD,
utilisation_table: [
{
member_id: string,
display_name: string,
allocated_hours: number,
available_hours: number,
pipeline_demand_hours: number,
total_demand_hours: number, // allocated + pipeline_demand
utilisation_pct: number, // (total_demand / available) * 100
status: 'overloaded' | 'underloaded' | 'ok',
flag_reason: string | null
}
],
overloaded_count: number,
underloaded_count: number,
requires_manager_review: boolean, // true if overloaded_count > 0
sheets_row_written: boolean,
data_quality_flags: string[] // passed through from collection agent
}- Google Sheets target: a named spreadsheet (internal name: 'Capacity Dashboard') with a tab named 'Weekly Log'. Each run appends one row per team member, keyed by week_start_date and member_id. Do not overwrite historical rows; always append.
- Utilisation formula: total_demand_hours = allocated_hours + pipeline_demand_hours. utilisation_pct = (total_demand_hours / available_hours) * 100. Round to one decimal place. If available_hours is 0 (full leave week), set utilisation_pct to null and flag_reason to 'full leave week'.
- Thresholds must be configurable. Store overload_threshold (default 100) and underload_threshold (default 60) as named ranges in the Sheets file so the operations manager can adjust them without touching the workflow.
- If any data_quality_flags are present in the input, append them to a 'QA Notes' column in the Sheets row for that week.
- Status colour coding in Sheets: use the Sheets API batchUpdate method with a RepeatCellRequest to set cell background: red for overloaded (utilisation_pct > 100), amber for underloaded (utilisation_pct < 60), green for ok. Apply to the utilisation_pct column only.
- requires_manager_review is set to true whenever overloaded_count > 0. The Reporting and Notification Agent uses this flag to decide between a full alert and a routine summary message.
- Build time estimate assumes the Sheets file and tab structure are created as part of this agent's setup phase. If an existing sheet is to be reused, field mapping must be confirmed before build.
This agent is the final delivery layer. It reads the analysis result and routes output to two audiences: the operations manager receives a structured Slack message with a full utilisation breakdown, overload flags, and a direct link to the Sheets dashboard; the leadership team receives an updated Notion page summarising overall team utilisation, headcount at risk, and any hiring or contractor flags surfaced by the data. The content and urgency of the Slack message varies based on the requires_manager_review flag from the Capacity Analysis Agent.
// Input
analysis_result: <object> // full output from Capacity Analysis Agent
sheets_url: string // deep link to the updated Sheets tab
// Output (Slack)
slack_message: {
channel: '#capacity-alerts', // confirm channel name with ops manager
recipient_user_id: string, // ops manager Slack user ID
message_type: 'alert' | 'routine',
overloaded_members: string[],
underloaded_members: string[],
pipeline_pressure_note: string | null,
sheets_link: string,
data_quality_warnings: string[]
}
// Output (Notion)
notion_page: {
page_id: string, // existing page ID to update, or parent_id to create under
title: 'Capacity Summary — Week of YYYY-MM-DD',
sections: [
'Team Utilisation Table',
'Overload Flags',
'Pipeline Pressure',
'Hiring or Contractor Flags',
'Data Quality Notes'
]
}- Slack: use Block Kit for message formatting. If requires_manager_review is true, open with a red warning header block. If false, use a green header. Always include the sheets_link as a button action block.
- Slack channel and ops manager user ID must be confirmed with the operations manager before build. Store both as environment variables, not hardcoded values.
- Notion integration requires a Notion internal integration token with insert content and update content permissions on the target database or page. Confirm the parent page ID where capacity summaries are to be nested.
- Notion page strategy: check whether a page with the matching week title already exists under the parent. If it exists, update in place using pages.update. If not, create a new child page using pages.create. This prevents duplicate pages on re-runs.
- If the Slack API call fails, log the error and retry once after 60 seconds before marking the run as failed. Do not suppress the error silently.
- Hiring or contractor flags are surfaced when overloaded_count >= 2 for two consecutive weeks. The agent must check the last two Sheets rows to evaluate this condition before composing the Notion page.
03End-to-end data flow
// ============================================================
// TRIGGER
// ============================================================
// Source: Weekly schedule (Monday 07:00) OR HubSpot webhook
// Webhook payload (Closed Won trigger):
// deal_id: string
// deal_stage: 'closedwon'
// close_date: YYYY-MM-DD
// resource_hours_estimate: number
trigger_event -> {
trigger_type: 'schedule' | 'hubspot_webhook',
trigger_timestamp: ISO8601,
week_start_date: YYYY-MM-DD,
hubspot_deal_id?: string
}
// ============================================================
// DATA COLLECTION AGENT — step 1: Asana task pull
// ============================================================
GET https://app.asana.com/api/1.0/tasks
?assignee=<asana_user_gid>
&project=<portfolio_project_gid>
&due_on.after=<week_start_date>
&due_on.before=<week_end_date>
&opt_fields=assignee,name,custom_fields,estimated_hours,completed
asana_response -> per_member: {
asana_user_id: string,
display_name: string,
tasks: [{ task_id, task_name, estimated_hours, due_on }],
allocated_hours: number // sum of estimated_hours across tasks
}
// DATA COLLECTION AGENT — step 2: Google Calendar leave pull
GET https://www.googleapis.com/calendar/v3/calendars/<member_email>/events
?timeMin=<week_start_date>T00:00:00Z
?timeMax=<week_end_date>T23:59:59Z
&singleEvents=true
&orderBy=startTime
calendar_response -> per_member: {
member_id: string,
leave_events: [{ event_id, summary, start, end, is_all_day }],
leave_hours: number // computed: all_day * 8 + partial_day duration
}
// DATA COLLECTION AGENT — step 3: HubSpot pipeline pull
POST https://api.hubapi.com/crm/v3/objects/deals/search
body: {
filterGroups: [{
filters: [
{ propertyName: 'dealstage', operator: 'IN', values: [<late_stage_ids>] },
{ propertyName: 'closedate', operator: 'LTE', value: <14_days_forward> }
]
}],
properties: ['dealname','closedate','resource_hours_estimate','hubspot_owner_id']
}
hubspot_response -> {
pipeline_deals: [
{
deal_id: string,
deal_name: string,
close_date: YYYY-MM-DD,
resource_hours_estimate: number,
hubspot_owner_id: string
}
]
}
// DATA COLLECTION AGENT — assembles capacity_dataset
capacity_dataset -> {
generated_at, week_start_date,
team_members: [{ member_id, display_name, asana_user_id,
allocated_hours, leave_hours, available_hours,
pipeline_demand_hours }],
pipeline_deals: [...],
data_quality_flags: [...]
}
// ============================================================
// HANDOFF: Data Collection Agent -> Capacity Analysis Agent
// ============================================================
// CAPACITY ANALYSIS AGENT — calculations
for each member in capacity_dataset.team_members:
total_demand_hours = allocated_hours + pipeline_demand_hours
utilisation_pct = (total_demand_hours / available_hours) * 100
status = utilisation_pct > 100 ? 'overloaded'
: utilisation_pct < 60 ? 'underloaded'
: 'ok'
// CAPACITY ANALYSIS AGENT — Sheets write
POST https://sheets.googleapis.com/v4/spreadsheets/<SHEET_ID>/values
/<TAB_NAME>!A:M:append
body: {
values: [[
week_start_date, member_id, display_name,
allocated_hours, leave_hours, available_hours,
pipeline_demand_hours, total_demand_hours,
utilisation_pct, status, flag_reason,
data_quality_flags, analysed_at
]]
}
analysis_result -> {
analysed_at, week_start_date,
utilisation_table: [...],
overloaded_count, underloaded_count,
requires_manager_review: boolean,
sheets_row_written: boolean,
data_quality_flags: [...]
}
// ============================================================
// HANDOFF: Capacity Analysis Agent -> Reporting and Notification Agent
// ============================================================
// REPORTING AGENT — Slack message
POST https://slack.com/api/chat.postMessage
body: {
channel: '#capacity-alerts',
blocks: [
{ type: 'header', text: requires_manager_review ? 'ALERT' : 'Capacity Ready' },
{ type: 'section', fields: [overloaded_members, underloaded_members] },
{ type: 'section', text: pipeline_pressure_note },
{ type: 'actions', elements: [{ type: 'button', text: 'View Sheet', url: sheets_url }] }
]
}
// REPORTING AGENT — Notion page update
PATCH https://api.notion.com/v1/pages/<notion_page_id>
body: {
properties: { title: 'Capacity Summary — Week of <week_start_date>' },
children: [
utilisation_table_block,
overload_flags_block,
pipeline_pressure_block,
hiring_flags_block,
data_quality_notes_block
]
}
// ============================================================
// END STATE
// ============================================================
// Ops manager has Slack alert with full utilisation breakdown
// Leadership has updated Notion page
// Google Sheets log row appended for trend tracking
// Human step: ops manager reviews, reallocates tasks in Asana if needed04Recommended build stack
More documents for this process
Every document generated for Capacity Planning.