Skip to main content

Asana

Asana is SABLE's project-management source of truth for tasks. It has two separate paths: a read-only mirror that keeps SABLE's tasks table current, and a gated write executor that creates Asana tasks only after a human approves the action. The two paths never overlap — the mirror cannot write to Asana, and the executor cannot browse it.

How it works

Mirror (asana-sync)

asana-sync authenticates with ASANA_API_KEY and walks the account:

  1. List workspaces. If the API key can see more than one, the caller (or the scheduled job) must pass workspaceGid explicitly.
  2. List every project in that workspace. A dry run samples up to 200 projects; a real run pages to completion.
  3. List every task in each project (completed_since pinned to the epoch, so completed tasks are included and can be reconciled).
  4. Match each Asana project to a SABLE project, in priority order:
    1. projects.asana_gid (already linked)
    2. A job code embedded in the Asana project name (regex \d{8}-\d+)
    3. Normalized project name
  5. Match each task's owner to a SABLE person, in priority order: assignee email, then assignee name. An unmatched owner name is kept as free text rather than defaulted onto the project lead.
  6. Upsert the result into tasks, keyed on (org_id, asana_gid).
  7. If — and only if — both the project list and the task scan for a project completed in full (no partial page), tasks that disappeared from Asana are closed in SABLE with a task_removed_from_workspace activity event. A partial scan never closes tasks, so a timeout or rate limit can't look like mass task completion.

A real (non-dry-run) sync additionally requires confirm:true in the request body — a second gate on top of admin/lead auth.

Act executor (asana-act)

asana-act executes exactly one approved action per call. A task is created in Asana only when all of the following hold:

  • The caller is an admin/lead (via Supabase JWT), or a cron/internal caller passing actorPersonId that resolves to an admin/lead person.
  • The approval belongs to the caller's org.
  • The approval's status is approved (not pending, denied, or already executed).
  • The approval's kind is asana_task_create.
  • The edge secret ASANA_ACT_WRITES_ENABLED is exactly "true".

On execution it claims the approval (approvedexecuting), resolves the intended owner against project members by email or a text custom field, creates the Asana task via REST, writes the returned asana_gid back onto the SABLE tasks row, and marks the approval executed (or failed with the error). If the approval carries a dar_id (a data-access-request kickoff flow), it also advances that request's status to requested. Every execution is logged to audit.

Configuration

SettingValue
SecretASANA_API_KEY (REST, used by both functions)
Write gateASANA_ACT_WRITES_ENABLED=true (edge secret; unset by default)
Scheduled workspaceASANA_WORKSPACE_GID (required env for the scheduled sync)

Cron cadence

TaskScheduleCalls
sable.asana.syncEvery 2 hours, :25 (25 */2 * * *)asana-sync with {confirm:true, limit:50, workspaceGid: <ASANA_WORKSPACE_GID>}

asana-act has no schedule — it only runs when a human approval triggers it.

Table mapping

TableConnectorWhat's written
tasksasana-syncUpsert on (org_id, asana_gid): text, owner, status, due date, source metadata, last-seen/verified timestamps
tasks.asana_gidasana-actSet once a created task's Asana id comes back
activity_eventsasana-synctask_removed_from_workspace when a full scan finds a task gone
connector_checkpointsasana-syncHealth/high-water-mark bookkeeping via the shared checkpoint helper
approvalsasana-actRead the approval, then advance its status
data_access_requestsasana-actStatus bumped to requested when the approval carries a dar_id
auditasana-actOne row per execution attempt

What can go wrong

  • A rate limit or timeout mid-scan looks like a stalled mirror, not a broken one. Because closing missing tasks requires a complete project-and-task scan, a partial run simply skips the close-out step rather than closing tasks incorrectly. Check connector_checkpoints for the asana row's health and error before assuming data loss.
  • Two Asana API keys see different workspace sets. If workspaceGid isn't pinned and the key can see more than one workspace, the sync intentionally requires the caller to disambiguate rather than silently picking one.
  • A created task never lands in Asana. Check ASANA_ACT_WRITES_ENABLED first — it ships unset, so writes are off until an operator explicitly flips it. Then confirm the approval's kind is exactly asana_task_create and its status is approved.
  • An owner shows up as free text instead of a linked person. This is intentional: an unmatched assignee name is kept as-is rather than defaulted onto the project lead, so it doesn't silently misattribute work. Fix the underlying email/name mismatch in Asana or in people.

MCP

agents/asana-agent.yaml points the agent's hosted-tool integration at Asana's own MCP endpoint rather than a SABLE-hosted proxy:

https://mcp.asana.com/v2/mcp

Least privilege here comes from Asana's own user/project permissions plus SABLE's human-approval gate on asana-act — the MCP endpoint itself has no additional scoping logic.

Where the code lives

  • supabase/functions/asana-sync/index.ts — mirror
  • supabase/functions/asana-act/index.ts — approved write executor
  • trigger/connectors.tssable.asana.sync schedule
  • agents/asana-agent.yaml — MCP endpoint configuration