Back to Capacity Planning

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

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

Step
Name
Description
1
Send Weekly Availability Request
Operations Manager sends a Slack message to all team members requesting confirmation of available hours for the coming week. Responses arrive in varying formats and at different times. Time cost: 20 min.
2
Chase Non-Responders
One or two team members consistently fail to respond by mid-week, requiring follow-up messages or calls before the picture is complete. BOTTLENECK. Time cost: 25 min.
3
Pull Confirmed Project Allocations
Manager opens Asana and manually copies each team member's assigned tasks and estimated hours into the capacity spreadsheet. BOTTLENECK. Time cost: 40 min.
4
Check Calendar for Leave and Absences
Google Calendar is reviewed per team member to identify annual leave, public holidays, or external commitments that reduce available hours. Time cost: 20 min.
5
Check Pipeline for Incoming Work
HubSpot is checked for late-stage deals likely to close this week or next so their estimated resource requirements can be factored into the plan. Time cost: 15 min.
6
Update Capacity Spreadsheet
All gathered data is manually entered into Google Sheets, which calculates utilisation percentages per person and flags anyone over or under threshold. BOTTLENECK. Time cost: 35 min.
7
Identify Overloaded and Underloaded Staff
Manager reads through the completed sheet and notes team members over 100% or under 60% utilisation, then decides what action is required. Time cost: 15 min.
8
Reallocate Tasks Where Possible
Tasks are manually shifted between team members in Asana, requiring skills and priority checks before moving anything. Time cost: 25 min. (Retained human step.)
9
Notify Leadership of Capacity Status
A summary is written and shared with the leadership team via a Notion page, including any hiring or contractor flags raised by the data. Time cost: 20 min.
10
File Capacity Record for Trend Tracking
The completed spreadsheet is saved to a shared drive folder for historical reference, with a manual date stamp in the filename. Time cost: 5 min.
Time cost summary: Total manual time per cycle is 220 minutes (3 hours 40 min). At approximately 4 cycles per month this equates to roughly 880 minutes (14.7 hours) per month, or 5 hours per week. The automation replaces steps 1, 2, 3, 4, 5 (Data Collection Agent), steps 6, 7, and 10 (Capacity Analysis Agent), and step 9 (Reporting and Notification Agent). Step 8 (task reallocation in Asana) is intentionally retained as a human decision step.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Data Collection Agent

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.

Trigger
Weekly schedule: Monday 07:00 local time OR HubSpot webhook on deal stage change to Closed Won.
Tools
Asana (Tasks API), Google Calendar (Events API), HubSpot (Deals API).
Replaces steps
1, 2, 3, 4, 5
Estimated build
14 hours — Moderate complexity
// 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.
Capacity Analysis Agent

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.

Trigger
Completion event from Data Collection Agent (internal workflow handoff, not an external webhook).
Tools
Google Sheets (Sheets API v4).
Replaces steps
6, 7, 10
Estimated build
10 hours — Moderate complexity
// 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.
Reporting and Notification Agent

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.

Trigger
Completion event from Capacity Analysis Agent (internal workflow handoff, not an external webhook).
Tools
Slack (Web API: chat.postMessage), Notion (Pages API: pages.update or pages.create).
Replaces steps
9
Estimated build
8 hours — Simple complexity
// 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.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full data journey from trigger to output — comment lines mark agent handoffs
// ============================================================
// 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 needed
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation tool with one workflow per agent (three workflows total): Data Collection Workflow, Capacity Analysis Workflow, and Reporting Workflow. All three workflows share a single credential store so OAuth tokens and API keys are maintained in one place and rotated once. Workflows are chained via internal execution triggers, not external webhooks, to avoid unnecessary public endpoints.
Webhook configuration
One public HTTPS webhook endpoint is required: the HubSpot Closed Won trigger. This endpoint must be registered in HubSpot under Settings > Integrations > Private Apps > Webhooks, subscribed to the deal.propertyChange event filtered on dealstage = closedwon. The endpoint URL is generated by the orchestration layer and must be stored in the credential store. The weekly schedule trigger is handled internally by the orchestration layer's scheduler (cron: 0 7 * * 1) and requires no external webhook.
Templating approach
Slack Block Kit JSON templates for the capacity alert message are stored as text assets within the orchestration layer and populated at runtime with values from the analysis_result object. Notion page structure is defined as a reusable block template in the Reporting Workflow. Both templates must be reviewed and signed off by the operations manager before go-live to confirm formatting and field labels.
Error logging
All agent errors (API failures, null data, schema mismatches, threshold config errors) are written to a Supabase table named automation_errors with columns: id (uuid), workflow_name (text), error_code (text), error_message (text), payload_snapshot (jsonb), created_at (timestamptz). A Slack alert to the #ops-alerts channel fires automatically when any row is inserted into this table, so the FullSpec team is notified of failures without polling. Supabase project credentials are stored in the shared credential store.
Testing approach
All agents are built and validated in a sandbox environment first. Asana and HubSpot both support sandbox or developer accounts; use these for initial API integration tests. Google Calendar testing uses a dedicated test calendar shared with the service account. For Google Sheets, a separate 'Capacity Dashboard - TEST' spreadsheet is used during build. Two full weeks of parallel running alongside the manual process are required before the manual process is retired, per the delivery plan.
Estimated total build time
Data Collection Agent: 14 hours. Capacity Analysis Agent: 10 hours. Reporting and Notification Agent: 8 hours. End-to-end integration testing and parallel run validation: 10 hours. Total: 42 hours.
Pre-build confirmation checklist: (1) Asana subscription is Business plan or higher and API access is enabled. (2) Google Calendar service account is created with domain-wide delegation and read access to all team member primary calendars. (3) HubSpot custom deal property 'resource_hours_estimate' exists and is populated on relevant deals. (4) Notion integration token has insert and update permissions on the target parent page. (5) Slack ops manager user ID and target channel name are confirmed. (6) Google Sheets 'Capacity Dashboard' file is created and shared with the service account. None of the three agents can be fully built until all six items are confirmed.
For any build questions, credential issues, or scope clarifications, contact the FullSpec team at support@gofullspec.com. Reference the process template ID 'operations-capacity-planning' in all correspondence.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Capacity Planning.

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