FS-DOC-05Technical
Integration and API Spec
Capacity Planning Automation
[YourCompany.com] · Operations Department · Prepared by FullSpec · [Today's Date]
This document defines every integration point in the Capacity Planning automation: the tools connected, the exact OAuth scopes required, webhook and polling configurations, field mappings between agents, credential store layout, and the full error handling and retry matrix. It is written for the FullSpec build team and covers all three agents: the Data Collection Agent (Agent 1), the Capacity Analysis Agent (Agent 2), and the Reporting and Notification Agent (Agent 3). Nothing in this document should be interpreted as final until sandbox validation is complete for every connection listed.
01Tool inventory
Tool
Role in automation
Auth method
Min plan required
Used by agent(s)
Asana
Source of task allocation and estimated hours data
OAuth 2.0
Business (API access required)
Agent 1
Google Calendar
Leave and absence detection per team member
OAuth 2.0 (Google Identity)
Google Workspace (any tier)
Agent 1
HubSpot
Late-stage pipeline demand and resource estimate input
OAuth 2.0 (Private App token)
Starter or above
Agent 1
Google Sheets
Capacity dashboard writes and utilisation trend log
OAuth 2.0 (Google Identity)
Google Workspace (any tier)
Agent 2
Slack
Ops manager alert delivery with structured capacity summary
OAuth 2.0 (Bot token)
Free or above
Agent 3
Notion
Leadership-facing weekly capacity summary page
OAuth 2.0 (Internal Integration token)
Free or above
Agent 3
Orchestration layer
Workflow automation platform hosting all three agent workflows, shared credential store, and scheduling
Platform-managed (service account or API key per tool)
N/A (platform choice TBC)
All agents
Before you connect anything: every tool connection must be validated against a sandbox or test environment before production credentials are stored. Create a dedicated test Asana project, a test Google Calendar, a HubSpot sandbox account, a staging Google Sheet, a private Slack channel, and a Notion test page. Run each integration end-to-end in the sandbox and confirm data shape matches the field mappings in Section 03 before switching to production credentials.
02Per-tool integration detail
Asana
Queried by Agent 1 (Data Collection Agent) to pull all active tasks assigned to team members for the coming week. Also the destination for manual reallocation decisions made by the Ops Manager in the retained human step.
Auth method
OAuth 2.0. Register a dedicated Asana app in the Asana Developer Console. Store the access token and refresh token in the credential store under ASANA_ACCESS_TOKEN and ASANA_REFRESH_TOKEN. Token lifetime is 1 hour; the orchestration layer must handle refresh automatically using the refresh token before each run.
Required scopes
default (read access to tasks, projects, users, and workspaces); openid; profile; email. No additional scopes are required for read-only task pulls. If the platform writes back to Asana (e.g. reallocation confirmation), add: tasks:write
Webhook / trigger setup
Agent 1 uses polling, not a webhook. On each scheduled run (Monday 07:00 local or HubSpot Closed Won trigger), the agent queries the Tasks API with a due_on filter covering the coming ISO week. No Asana webhook registration is required. Polling interval: once per trigger event only, not continuous.
Required configuration
Store in credential store: ASANA_WORKSPACE_GID (the GID of the primary workspace), ASANA_PROJECT_GIDS (comma-separated list of project GIDs to query; do not hardcode). Confirm that all staff tasks include the custom field estimated_hours or that the native due_on plus duration fields are populated consistently. Agree field mapping with the Ops Manager before build.
Rate limits
Asana enforces 1,500 requests per minute per OAuth token. A single Agent 1 run pulling tasks for up to 20 staff across multiple projects will generate approximately 25 to 60 API calls. At current volume of 4 runs per month plus ad-hoc HubSpot triggers (estimated 8 to 12 additional runs per month), total monthly API calls are well under 2,000. Throttling is NOT required at current volume. Add a 200ms inter-request delay as a precaution.
Constraints
Asana Business plan or above is mandatory for full API access. The free plan restricts API capabilities and will cause task pulls to return incomplete data. Confirm subscription tier before build begins. Pagination applies to task list responses (100 tasks per page); the agent must follow the next_page.offset token until exhausted.
// Request: GET /tasks
GET https://app.asana.com/api/1.0/tasks
?workspace={ASANA_WORKSPACE_GID}
&assignee=any
&due_on.after={week_start_date}
&due_on.before={week_end_date}
&opt_fields=gid,name,assignee.name,assignee.gid,due_on,custom_fields
&limit=100
// Response shape (per task):
{
"gid": "1234567890",
"name": "Deliver client proposal",
"assignee": { "gid": "9876543210", "name": "Jordan Lee" },
"due_on": "2025-06-09",
"custom_fields": [
{ "name": "estimated_hours", "number_value": 4.5 }
]
}Google Calendar
Queried by Agent 1 to detect leave events, public holidays, and external blocks for each team member, reducing their available hours in the capacity dataset.
Auth method
OAuth 2.0 via Google Identity Platform. Use a service account with domain-wide delegation enabled to read all team member calendars without individual user consent prompts. Store the service account JSON key in the credential store under GCAL_SERVICE_ACCOUNT_JSON. Alternatively, use a user OAuth token if domain-wide delegation is not available; in that case each calendar must be explicitly shared with the automation service account.
Required scopes
https://www.googleapis.com/auth/calendar.readonly
Webhook / trigger setup
Agent 1 polls the Calendar API on each scheduled run. No push notification channel is registered. The agent iterates over the list of team member calendar IDs stored in GCAL_TEAM_CALENDAR_IDS and calls the Events endpoint for each, filtering to the coming week's date range.
Required configuration
Store in credential store: GCAL_SERVICE_ACCOUNT_JSON (full JSON key), GCAL_TEAM_CALENDAR_IDS (JSON array of {staff_name, calendar_id} objects). Leave events must follow a consistent naming convention agreed with the Ops Manager (e.g. events beginning with 'LEAVE:' or 'OOO' are treated as full-day absences). Store the leave keyword patterns in GCAL_LEAVE_KEYWORDS. Standard working hours per day (e.g. 8) stored in GCAL_HOURS_PER_DAY.
Rate limits
Google Calendar API: 1,000,000 queries per day and 500 requests per 100 seconds per project. A single run for 20 staff generates 20 Events API calls. At current volume this is negligible. Throttling is NOT required. Add a 100ms inter-request delay as a courtesy.
Constraints
Leave detection accuracy depends entirely on team members booking leave as calendar events using the agreed naming convention. If leave is tracked in an HR system (e.g. BambooHR, Breathe), a separate integration point will be required and is out of scope for the Standard build. Public holidays must be added to GCAL_PUBLIC_HOLIDAY_CALENDAR_ID if not already visible on team calendars.
// Request: GET /calendars/{calendarId}/events
GET https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events
?timeMin={week_start_date}T00:00:00Z
?timeMax={week_end_date}T23:59:59Z
&singleEvents=true
&orderBy=startTime
// Response shape (per event):
{
"summary": "LEAVE: Annual Leave",
"start": { "date": "2025-06-09" },
"end": { "date": "2025-06-11" },
"status": "confirmed"
}HubSpot
Queried by Agent 1 to fetch late-stage deals and their associated resource hour estimates, which are added to the incoming demand column of the capacity dataset.
Auth method
OAuth 2.0 via HubSpot Private App. Create a Private App in HubSpot Settings and generate an access token. Store in credential store as HUBSPOT_PRIVATE_APP_TOKEN. Private App tokens do not expire but should be rotated every 90 days per security policy. Do not use legacy API keys.
Required scopes
crm.objects.deals.read; crm.schemas.deals.read
Webhook / trigger setup
Two trigger modes apply. Mode 1: On the weekly Monday schedule, Agent 1 polls HubSpot for all deals in pipeline stages defined in HUBSPOT_LATE_STAGE_IDS with a close date within the next 14 days. Mode 2: HubSpot sends a webhook to the orchestration layer's inbound webhook endpoint when a deal property deal_stage changes to the Closed Won stage ID (stored in HUBSPOT_CLOSED_WON_STAGE_ID). The orchestration layer must validate the webhook using the HubSpot signature header X-HubSpot-Signature-Version: v3 and the client secret stored in HUBSPOT_WEBHOOK_SECRET.
Required configuration
Store in credential store: HUBSPOT_PRIVATE_APP_TOKEN, HUBSPOT_PORTAL_ID, HUBSPOT_LATE_STAGE_IDS (JSON array of pipeline stage IDs considered 'late stage', e.g. Proposal Sent, Contract Sent, Closed Won), HUBSPOT_CLOSED_WON_STAGE_ID, HUBSPOT_RESOURCE_HOURS_PROPERTY (the internal name of the custom deal property holding resource hour estimates, e.g. 'estimated_resource_hours'), HUBSPOT_WEBHOOK_SECRET. If HUBSPOT_RESOURCE_HOURS_PROPERTY does not yet exist, it must be created in HubSpot and back-filled before the integration is meaningful.
Rate limits
HubSpot CRM API: 100 requests per 10 seconds and 250,000 requests per day (Starter plan). A single Agent 1 poll generates 1 to 3 API calls depending on pagination. At current volume (approximately 16 trigger events per month) this is negligible. Throttling is NOT required.
Constraints
The resource hour estimate custom property must be populated on deals before the integration adds value. Deals without this property populated will be included in the count but will contribute 0 hours to demand, and Agent 2 must handle this gracefully with a null-safe default of 0. Webhook delivery from HubSpot is not guaranteed to arrive in under 30 seconds; Agent 1 must tolerate a delay of up to 5 minutes between the deal stage change and the automation run.
// Request: POST /crm/v3/objects/deals/search (late-stage poll)
POST https://api.hubapi.com/crm/v3/objects/deals/search
Authorization: Bearer {HUBSPOT_PRIVATE_APP_TOKEN}
{
"filterGroups": [{
"filters": [
{ "propertyName": "dealstage", "operator": "IN",
"values": ["{HUBSPOT_LATE_STAGE_IDS}"] },
{ "propertyName": "closedate", "operator": "LTE",
"value": "{14_days_from_now_epoch_ms}" }
]
}],
"properties": ["dealname", "dealstage", "closedate",
"{HUBSPOT_RESOURCE_HOURS_PROPERTY}"],
"limit": 100
}
// Webhook payload (Closed Won trigger):
{
"subscriptionType": "deal.propertyChange",
"portalId": "{HUBSPOT_PORTAL_ID}",
"objectId": 12345,
"propertyName": "dealstage",
"propertyValue": "{HUBSPOT_CLOSED_WON_STAGE_ID}"
}Integration and API SpecPage 1 of 4
FS-DOC-05Technical
Google Sheets
Written to by Agent 2 (Capacity Analysis Agent). Receives the fully calculated utilisation table, status flags, and trend row. Serves as the persistent capacity dashboard and historical log.
Auth method
OAuth 2.0 via Google Identity Platform. Use the same service account as Google Calendar (with domain-wide delegation) or a dedicated service account with the Sheet shared to its email address. Store credentials under GSHEETS_SERVICE_ACCOUNT_JSON. Recommended: use a single shared Google service account for both Calendar and Sheets to reduce credential surface.
Required scopes
https://www.googleapis.com/auth/spreadsheets
Webhook / trigger setup
Google Sheets does not support outbound webhooks natively. Agent 2 is triggered by Agent 1 completing its dataset, not by a Sheets event. The orchestration layer passes the dataset directly between agents via an internal payload; no Sheets polling is needed to trigger Agent 2.
Required configuration
Store in credential store: GSHEETS_SPREADSHEET_ID (the ID of the master capacity dashboard sheet, taken from its URL), GSHEETS_DATA_RANGE (e.g. 'Capacity!A2:H' for the main data table), GSHEETS_TREND_RANGE (e.g. 'Trend!A:Z' for the week-on-week log). Sheet column structure must be fixed and documented: Column A: staff_name, B: available_hours, C: allocated_hours, D: leave_hours, E: pipeline_demand_hours, F: utilisation_pct, G: status_flag (OVERLOADED / OK / UNDERLOADED), H: week_start_date. Do not allow end users to insert or delete columns without updating these range references.
Rate limits
Google Sheets API: 300 requests per minute per project and 60 requests per minute per user. A single Agent 2 write covering 20 staff rows is 1 to 2 API calls (batch update). At 4 to 16 runs per month, usage is negligible. Throttling is NOT required.
Constraints
All writes must use batchUpdate to minimise API calls and avoid partial-write failures. Existing formulas in any cell within GSHEETS_DATA_RANGE will be overwritten on each run; any end-user formulas must be placed in columns outside the write range. Conditional formatting (colour coding) should be applied as a one-time Sheet setup, not written by the automation on each run.
// Request: POST /spreadsheets/{id}/values/{range}:clear then batchUpdate
POST https://sheets.googleapis.com/v4/spreadsheets
/{GSHEETS_SPREADSHEET_ID}/values:batchUpdate
{
"valueInputOption": "USER_ENTERED",
"data": [{
"range": "{GSHEETS_DATA_RANGE}",
"values": [
["Jordan Lee", 40, 38, 0, 6, "110%", "OVERLOADED", "2025-06-09"],
["Sam Patel", 40, 22, 8, 0, "55%", "UNDERLOADED", "2025-06-09"]
]
}]
}Slack
Used by Agent 3 (Reporting and Notification Agent) to deliver a structured capacity alert to the operations manager's Slack channel immediately after the Google Sheets write is confirmed.
Auth method
OAuth 2.0 Bot token. Install the automation's Slack app to the workspace with bot scopes. Store the bot token in the credential store as SLACK_BOT_TOKEN. Bot tokens do not expire unless revoked. The app must be added to the target channel by a Slack admin before the first run.
Required scopes
chat:write; chat:write.public (if posting to public channels); channels:read (to resolve channel name to ID at setup)
Webhook / trigger setup
Agent 3 posts to Slack using the chat.postMessage API method, not an incoming webhook. This allows structured Block Kit message formatting. The destination channel ID is stored in SLACK_OPS_CHANNEL_ID. No inbound Slack events or slash commands are used in this automation.
Required configuration
Store in credential store: SLACK_BOT_TOKEN, SLACK_OPS_CHANNEL_ID (the channel ID, not the name, for the ops manager alert), SLACK_CAPACITY_SHEET_URL (the direct URL to the Google Sheet for inclusion in the message). The Slack message template uses Block Kit with a header section, a two-column fields section listing overloaded and underloaded staff, and a button linking to the Sheet. The template structure is defined in the orchestration layer configuration and must not be hardcoded in the script.
Rate limits
Slack API: 1 message per second on chat.postMessage (Tier 3, burst allowed). Each Agent 3 run sends 1 message. Throttling is NOT required at any foreseeable volume.
Constraints
Slack bot must be invited to SLACK_OPS_CHANNEL_ID before deployment. The message must include a fallback text field for notification previews. If the channel is archived or the bot is removed, the Slack call will return a channel_not_found error; this must trigger the error handling behaviour defined in Section 05.
// Request: POST /chat.postMessage
POST https://slack.com/api/chat.postMessage
Authorization: Bearer {SLACK_BOT_TOKEN}
{
"channel": "{SLACK_OPS_CHANNEL_ID}",
"text": "Weekly Capacity Alert: 2 staff overloaded",
"blocks": [
{ "type": "header", "text": {
"type": "plain_text",
"text": "Capacity Alert: Week of 9 Jun 2025" }},
{ "type": "section", "fields": [
{ "type": "mrkdwn", "text": "*Overloaded (>100%)*\nJordan Lee: 110%" },
{ "type": "mrkdwn", "text": "*Underloaded (<60%)*\nSam Patel: 55%" }
]},
{ "type": "actions", "elements": [
{ "type": "button", "text": { "type": "plain_text",
"text": "Open Capacity Sheet" },
"url": "{SLACK_CAPACITY_SHEET_URL}" }
]}
]
}Notion
Used by Agent 3 to create or update the weekly capacity summary page visible to the leadership team. The page is either created fresh each week under a designated parent page or an existing page is updated via its page ID.
Auth method
OAuth 2.0 via Notion Internal Integration. Create an Internal Integration in Notion's integration settings, copy the Integration Token, and share the target parent page (or database) with the integration. Store the token in the credential store as NOTION_INTEGRATION_TOKEN.
Required scopes
Notion Internal Integrations use capability toggles rather than OAuth scope strings. Required capabilities: Read content, Update content, Insert content. The integration must be granted access explicitly to the parent page or database in Notion's sharing settings.
Webhook / trigger setup
Notion does not support outbound webhooks. Agent 3 writes to Notion via the Pages API and Blocks API. No inbound Notion events are used. The agent is triggered by the completion of the Slack post in the same Agent 3 workflow.
Required configuration
Store in credential store: NOTION_INTEGRATION_TOKEN, NOTION_PARENT_PAGE_ID (the page ID of the 'Capacity Reports' parent page under which weekly pages are created), NOTION_DATABASE_ID (if using a Notion database rather than a page hierarchy; leave empty if using page-based approach). The weekly page title format is: 'Capacity Summary: {week_start_date}'. Page content must include: utilisation table (staff name, utilisation %, status), pipeline pressure section, and any hiring or contractor flags. Page structure is defined as a JSON block template in the orchestration layer configuration.
Rate limits
Notion API: 3 requests per second average. Creating a page with a utilisation table for 20 staff generates approximately 4 to 6 API calls. At current volume, throttling is NOT required. Add a 350ms inter-request delay to stay safely within the rate limit.
Constraints
The Notion integration must be re-shared whenever the parent page is moved or duplicated. If NOTION_PARENT_PAGE_ID becomes invalid, the page creation call returns a object_not_found error. Integration tokens for Internal Integrations do not expire but should be rotated every 90 days. Notion block content has a 2,000 character limit per rich text element; staff summaries must be truncated or split if they exceed this limit.
// Request: POST /pages (create weekly summary page)
POST https://api.notion.com/v1/pages
Authorization: Bearer {NOTION_INTEGRATION_TOKEN}
Notion-Version: 2022-06-28
{
"parent": { "page_id": "{NOTION_PARENT_PAGE_ID}" },
"properties": {
"title": [{ "text": {
"content": "Capacity Summary: 2025-06-09" } }]
},
"children": [
{ "object": "block", "type": "heading_2",
"heading_2": { "rich_text": [{ "text": {
"content": "Team Utilisation" } }] } },
{ "object": "block", "type": "table",
"table": { "table_width": 3,
"has_column_header": true, "has_row_header": false,
"children": [ ... ] } }
]
}Integration and API SpecPage 2 of 4
FS-DOC-05Technical
03Field mappings between tools
The tables below define every field transformation between agent handoffs. Use exact field names as shown (monospace). Any rename, type cast, or null-safe default is noted in the Destination field column.
Agent 1 handoff: Asana, Google Calendar, and HubSpot to the Agent 1 internal capacity dataset
Source tool
Source field
Destination tool
Destination field
Asana
`task.assignee.name`
Agent 1 dataset
`staff_name` (string)
Asana
`task.assignee.gid`
Agent 1 dataset
`staff_id` (string, used as join key)
Asana
`task.custom_fields[estimated_hours].number_value`
Agent 1 dataset
`allocated_hours` (float; null defaults to 0)
Asana
`task.due_on`
Agent 1 dataset
`task_due_date` (ISO date string; used to confirm within target week)
Google Calendar
`event.summary`
Agent 1 dataset
`leave_event_title` (string; matched against GCAL_LEAVE_KEYWORDS)
Google Calendar
`event.start.date`
Agent 1 dataset
`leave_start_date` (ISO date)
Google Calendar
`event.end.date`
Agent 1 dataset
`leave_end_date` (ISO date; exclusive end per Google API convention, adjust by minus 1 day)
Google Calendar (derived)
Days between `leave_start_date` and `leave_end_date`
Agent 1 dataset
`leave_hours` (float; days multiplied by GCAL_HOURS_PER_DAY)
HubSpot
`deal.properties.estimated_resource_hours`
Agent 1 dataset
`pipeline_demand_hours` (float; null defaults to 0; summed across all late-stage deals)
HubSpot
`deal.properties.dealname`
Agent 1 dataset
`pipeline_deal_names` (string array; included in Slack alert for context)
Agent 1 to Agent 2 handoff: internal capacity dataset to Capacity Analysis Agent
Source tool
Source field
Destination tool
Destination field
Agent 1 dataset
`staff_name`
Agent 2 / Google Sheets
`staff_name` (Column A)
Agent 1 dataset
`available_hours` (GCAL_HOURS_PER_DAY multiplied by working days in week, minus `leave_hours`)
Agent 2 / Google Sheets
`available_hours` (Column B, float)
Agent 1 dataset
`allocated_hours` (summed across all tasks per staff member)
Agent 2 / Google Sheets
`allocated_hours` (Column C, float)
Agent 1 dataset
`leave_hours`
Agent 2 / Google Sheets
`leave_hours` (Column D, float)
Agent 1 dataset
`pipeline_demand_hours` (total across all deals, divided equally across staff unless staff assignment field exists on deal)
Agent 2 / Google Sheets
`pipeline_demand_hours` (Column E, float)
Agent 2 (calculated)
`(allocated_hours + pipeline_demand_hours) / available_hours * 100`
Google Sheets
`utilisation_pct` (Column F, string formatted as percentage, e.g. '110%')
Agent 2 (calculated)
OVERLOADED if `utilisation_pct` > 100; UNDERLOADED if < 60; OK otherwise
Google Sheets
`status_flag` (Column G, string)
Agent 2 (system)
Run date (ISO week start)
Google Sheets
`week_start_date` (Column H, ISO date string)
Agent 2 to Agent 3 handoff: Google Sheets results to Slack and Notion
Source tool
Source field
Destination tool
Destination field
Google Sheets
`staff_name` where `status_flag` = OVERLOADED
Slack Block Kit message
`overloaded_staff_list` (mrkdwn field, comma-separated with utilisation %)
Google Sheets
`staff_name` where `status_flag` = UNDERLOADED
Slack Block Kit message
`underloaded_staff_list` (mrkdwn field, comma-separated with utilisation %)
Google Sheets
`utilisation_pct` (all rows)
Slack Block Kit message
`team_avg_utilisation` (derived average, displayed in header section)
Agent 1 dataset
`pipeline_deal_names`
Slack Block Kit message
`pipeline_context` (mrkdwn section listing deal names driving demand)
Google Sheets
`staff_name`, `allocated_hours`, `available_hours`, `utilisation_pct`, `status_flag`
Notion page blocks
Table rows: columns Staff, Allocated hrs, Available hrs, Utilisation, Status
Agent 1 dataset
`pipeline_demand_hours` (total)
Notion page blocks
Paragraph block under heading 'Pipeline Pressure' (string, e.g. '14 hrs incoming')
Agent 2 (calculated)
Count of OVERLOADED staff, count of UNDERLOADED staff
Notion page blocks
Callout block under heading 'Action Required' (shown only if count > 0)
04Build stack and orchestration
Orchestration layout
Three separate workflow definitions in the automation platform: one per agent. Workflows are chained sequentially: Agent 1 completion triggers Agent 2; Agent 2 completion triggers Agent 3. Each workflow shares a single credential store scoped to this automation. No cross-automation credential sharing.
Agent 1 trigger mechanism
Dual trigger: (1) Scheduled cron, every Monday at 07:00 in the operations manager's local timezone (configured as America/New_York or equivalent; store timezone in config as SCHEDULE_TIMEZONE). (2) Inbound webhook listener registered in the orchestration layer, receiving HubSpot deal stage change events. Webhook signature validated using HMAC-SHA256 with HUBSPOT_WEBHOOK_SECRET before any processing begins. If signature validation fails, return HTTP 400 and log the attempt; do not proceed.
Agent 2 trigger mechanism
Internal event trigger from Agent 1. When Agent 1 completes and writes its dataset to the orchestration layer's internal data store, it emits a completion event that Agent 2's workflow listens for. Agent 2 does not poll; it is invoked directly. No external webhook involved.
Agent 3 trigger mechanism
Internal event trigger from Agent 2. Fired when Agent 2 confirms a successful batchUpdate write to Google Sheets (HTTP 200 response with updatedRows > 0). Agent 3 then posts to Slack and Notion in sequence. Slack post is attempted first; Notion post proceeds regardless of Slack outcome (failures handled independently per Section 05).
Credential store scope
All secrets stored in the automation platform's encrypted credential store, scoped to this automation only. No credentials stored in workflow step configurations, environment variables visible in logs, or hardcoded strings. Credential references use the variable names defined below.
Retry and state
Agent 1 is stateless per run; it does not cache previous results. Agent 2 overwrites the Google Sheet on each run (no append-only mode unless trend logging is active on GSHEETS_TREND_RANGE). Agent 3 is idempotent for Slack (duplicate messages are acceptable; the ops manager can dismiss). Notion page creation is NOT idempotent by default; the agent must check for an existing page with the same title under NOTION_PARENT_PAGE_ID before creating, and update rather than duplicate.
Credential store: all variable names for the Capacity Planning automation
// Credential store: variable names and descriptions
// All values injected at runtime; never logged or echoed
ASANA_ACCESS_TOKEN // OAuth access token for Asana API
ASANA_REFRESH_TOKEN // OAuth refresh token; used to renew ASANA_ACCESS_TOKEN
ASANA_WORKSPACE_GID // Asana workspace GID (from URL: app.asana.com/0/{GID})
ASANA_PROJECT_GIDS // JSON array of project GIDs to query for tasks
GCAL_SERVICE_ACCOUNT_JSON // Full JSON key for Google service account
GCAL_TEAM_CALENDAR_IDS // JSON array: [{staff_name, calendar_id}, ...]
GCAL_LEAVE_KEYWORDS // JSON array of strings: ['LEAVE:', 'OOO', 'Annual Leave']
GCAL_HOURS_PER_DAY // Integer: standard working hours per day (e.g. 8)
GCAL_PUBLIC_HOLIDAY_CALENDAR_ID // Google Calendar ID for public holidays (may be null)
HUBSPOT_PRIVATE_APP_TOKEN // HubSpot Private App access token
HUBSPOT_PORTAL_ID // HubSpot portal/account ID
HUBSPOT_LATE_STAGE_IDS // JSON array of pipeline stage IDs classed as late-stage
HUBSPOT_CLOSED_WON_STAGE_ID // Pipeline stage ID for Closed Won (webhook trigger match)
HUBSPOT_RESOURCE_HOURS_PROPERTY // Internal name of resource hours deal property
HUBSPOT_WEBHOOK_SECRET // Client secret for X-HubSpot-Signature-Version: v3 HMAC
GSHEETS_SERVICE_ACCOUNT_JSON // Full JSON key (can share with GCAL if same account)
GSHEETS_SPREADSHEET_ID // Google Sheets file ID (from URL)
GSHEETS_DATA_RANGE // Range string, e.g. 'Capacity!A2:H'
GSHEETS_TREND_RANGE // Range string for historical log, e.g. 'Trend!A:Z'
SLACK_BOT_TOKEN // Slack bot OAuth token (xoxb-...)
SLACK_OPS_CHANNEL_ID // Slack channel ID for ops manager alerts (e.g. C01ABCDEF)
SLACK_CAPACITY_SHEET_URL // Direct URL to the Google Sheet for Slack button
NOTION_INTEGRATION_TOKEN // Notion Internal Integration token (secret_...)
NOTION_PARENT_PAGE_ID // Page ID of 'Capacity Reports' parent page in Notion
NOTION_DATABASE_ID // Optional: Notion database ID if using DB over pages
SCHEDULE_TIMEZONE // Timezone string for cron schedule, e.g. America/New_York
OVERLOAD_THRESHOLD_PCT // Integer: utilisation % above which staff are OVERLOADED (e.g. 100)
UNDERLOAD_THRESHOLD_PCT // Integer: utilisation % below which staff are UNDERLOADED (e.g. 60)OVERLOAD_THRESHOLD_PCT and UNDERLOAD_THRESHOLD_PCT must be stored as configuration variables in the credential store, not hardcoded in the calculation logic. The Ops Manager may need to adjust these thresholds as the team grows or business model changes. A code-level change to update a threshold is not acceptable.
Integration and API SpecPage 3 of 4
FS-DOC-05Technical
05Error handling and retry logic
Every integration point has a defined failure behaviour. Unhandled exceptions must never fail silently. All errors must be logged to the orchestration layer's run log with the integration name, error code, timestamp, and the payload that caused the failure. Where a manual fallback is defined, a notification must be sent to support@gofullspec.com and to SLACK_OPS_CHANNEL_ID.
Integration
Scenario
Required behaviour
Asana (Agent 1)
401 Unauthorized: access token expired
Attempt token refresh using ASANA_REFRESH_TOKEN. If refresh succeeds, retry the failed request immediately. If refresh fails (invalid_grant), halt Agent 1, log the error, and send a manual fallback alert to the Ops Manager via Slack and to support@gofullspec.com. Do not proceed to Agent 2.
Asana (Agent 1)
429 Too Many Requests
Respect the Retry-After header. Wait the specified number of seconds (default 60 if header absent) and retry the request. Retry up to 3 times with exponential backoff (60s, 120s, 240s). If all retries exhausted, log and send manual fallback alert. Do not proceed to Agent 2.
Asana (Agent 1)
Task list returns 0 results
This may indicate a misconfigured ASANA_PROJECT_GIDS or an unusually quiet week. Log a warning. Proceed with allocated_hours = 0 for all staff. Include a data-quality warning in the Slack alert sent by Agent 3 ('No Asana tasks found for this week; data may be incomplete').
Google Calendar (Agent 1)
403 Forbidden: service account lacks delegation
Halt Agent 1. Log the error with the affected calendar_id. Send manual fallback alert. This error requires an admin to re-enable domain-wide delegation; it cannot be self-healed at runtime.
Google Calendar (Agent 1)
Individual calendar fetch fails (404 or 403 for one team member)
Log a warning for that team member. Set leave_hours = 0 for the affected staff member and continue processing remaining calendars. Include a data-quality note in the Slack alert listing any staff members whose calendar could not be read.
HubSpot webhook (Agent 1 trigger)
Signature validation fails (X-HubSpot-Signature mismatch)
Return HTTP 400 immediately. Do not process the payload. Log the source IP, timestamp, and raw payload hash. Send an alert to support@gofullspec.com. If more than 3 signature failures occur within 1 hour, escalate to the FullSpec security review process.
HubSpot API (Agent 1)
HUBSPOT_RESOURCE_HOURS_PROPERTY is null or missing on a deal
Set pipeline_demand_hours = 0 for that deal. Log a warning with the deal ID and name. Include the affected deal names in a 'Missing resource data' section of the Slack alert so the Ops Manager can back-fill the property.
Google Sheets (Agent 2)
batchUpdate returns non-200 or updatedRows = 0
Retry once after 30 seconds. If the second attempt fails, halt Agent 2 and Agent 3. Log the full API response. Send a manual fallback alert: 'Capacity dashboard could not be updated this week; manual update required.' Do not send the Slack capacity alert without a confirmed Sheets write, as the data would be stale.
Slack (Agent 3)
channel_not_found or not_in_channel error
Log the error. Attempt to post to a fallback channel defined in config (SLACK_FALLBACK_CHANNEL_ID, e.g. #general or the FullSpec monitoring channel). If the fallback also fails, send the capacity summary by email to the Ops Manager's address stored in OPS_MANAGER_EMAIL. Do not suppress the notification.
Slack (Agent 3)
429 Rate limited
Wait for the Retry-After value (typically 1 second) and retry once. Slack rate limits on chat.postMessage are generous; a single message should not be rate limited under normal conditions. If the retry fails, fall back to email as above.
Notion (Agent 3)
object_not_found for NOTION_PARENT_PAGE_ID
Log the error with the invalid page ID. Send a manual fallback alert: 'Notion capacity page could not be created; the parent page may have been moved or deleted. Please update NOTION_PARENT_PAGE_ID in the credential store.' Do not halt the Slack alert; Notion failure is non-blocking for Agent 3.
Notion (Agent 3)
Duplicate page already exists for this week
Before creating, query children of NOTION_PARENT_PAGE_ID for a page title matching 'Capacity Summary: {week_start_date}'. If found, update the existing page using PATCH /pages/{page_id} rather than creating a new one. This ensures idempotency on re-runs.
Orchestration layer (any agent)
Agent times out (execution exceeds platform maximum)
Log the timeout with the last completed step. Send a manual fallback alert to the Ops Manager and to support@gofullspec.com. The run must be manually re-triggered after investigating the cause. Partial writes to Google Sheets during a timeout must be cleared before re-running to avoid duplicate rows in GSHEETS_TREND_RANGE.
Unhandled exceptions must never fail silently. Any error not covered by the table above must be caught by a global exception handler in each agent workflow, logged with full context (agent name, step, error message, timestamp, payload snapshot), and trigger an alert to support@gofullspec.com. A generic 'run failed' notification to the Ops Manager via Slack or email must always fire, even if the specific error cannot be categorised.
Integration and API SpecPage 4 of 4