Asana tracker adapter¶
Bernstein can pull work from an Asana project, post progress stories back to the task, and move the task between sections when a task transitions. The adapter also supports per-step CLI choice: an operator-declared custom field can name the CLI adapter (claude, codex, aider, ...) Bernstein should pick for each task.
The adapter implements bernstein.core.trackers.AbstractTrackerAdapter and lives under bernstein.core.trackers.builtin.asana_adapter. It is disabled by default and must be opted in via bernstein.yaml.
Auth¶
Authentication uses an Asana Personal Access Token (PAT). Point the adapter at the environment variable that holds the token. If pat_env is omitted the adapter falls back to ASANA_PERSONAL_ACCESS_TOKEN.
trackers:
asana:
workspace_gid: "1199999999999999"
project_gid: "1201234567890123"
auth:
pat_env: ASANA_PERSONAL_ACCESS_TOKEN
A PAT with read+write access on the workspace is sufficient. See the Asana developer docs for how to mint one.
Sections vs. status¶
Asana does not have a global "status" surface. Sections are per-project workflow buckets and act as the status surface for this adapter. Section gids are operator-supplied because they cannot be inferred without the schema.
trackers:
asana:
workspace_gid: "1199999999999999"
project_gid: "1201234567890123"
section_filter_gid: "1207770000000001" # default section to pull
section_map:
ready: "1207770000000001"
claimed: "1207770000000002"
done: "1207770000000003"
failed: "1207770000000004"
transition(ticket_id, status_id) resolves status_id in this order:
- Treat the value as a section gid (opaque).
- Look it up in
section_map.
If neither resolves, TrackerUnavailable is raised.
Per-step CLI choice¶
Add a single-select custom field whose enum values name the CLI (claude, codex, aider). Point the adapter at the field's gid:
trackers:
asana:
workspace_gid: "1199999999999999"
project_gid: "1201234567890123"
cli_choice_custom_field_gid: "1207770099999999"
The adapter exposes the field value on the ticket dataclass:
from bernstein.core.trackers.builtin import AsanaAdapter, AsanaConfig
config = AsanaConfig(
workspace_gid="1199999999999999",
project_gid="1201234567890123",
section_filter_gid="1207770000000001",
cli_choice_custom_field_gid="1207770099999999",
pat_env="ASANA_PERSONAL_ACCESS_TOKEN",
)
with AsanaAdapter(config) as adapter:
for ticket in adapter.pull_open_tickets():
cli = ticket.routing_hint.cli # "claude" / "codex" / "aider" / None
When the orchestrator's routing layer sees a non-empty ticket.routing_hint.cli, it pins the spawned CLI for that ticket.
Example workflow¶
A common ops-team setup keeps four sections in one Asana project:
| Section | Bernstein status | Meaning |
|---|---|---|
| Ready | ready | Available for an agent to claim. |
| In flight | claimed | An agent has picked it up. |
| Done | done | Acceptance criteria green. |
| Failed | failed | Hard halt, operator follow-up. |
A custom field Agent (text or enum) lets the requester pin a specific CLI per task. Stories (comments) on the task carry the agent's status updates back to the requester without leaving the Asana UI.
Comments and transitions¶
add_comment(ticket_id, body)posts a story to the task. Passticket.id(the task gid) asticket_id.transition(ticket_id, status_id)moves the task into the target section viaPOST /sections/{section_gid}/addTask.
Both methods accept an idempotency_key that is forwarded as an X-Idempotency-Key request header. The server ignores unknown headers; operator-side logging can use the value to correlate retries.
Custom-field writes¶
For non-section custom-field writes, call adapter.update_custom_field(ticket_id, custom_field_gid, value). The accepted shape of value depends on the custom-field type (text, number, enum option gid, etc.) and matches the body shape documented in the Asana API reference.
Rate limiting¶
- A cooperative per-adapter token-bucket throttles outbound calls when
rate_limit_min_intervalis set. - A
429response is translated intoRateLimited(retry_after=...). The hint is taken from theRetry-Afterheader when present.
Exceptions¶
| Exception | When |
|---|---|
TrackerUnavailable | 4xx (other than 429), 5xx, malformed JSON, payload errors, missing token, empty section gid. |
RateLimited | 429. |
Out of scope¶
- Portfolios and goals (separate adapter shape if needed).
- Asana Forms (we read tasks, not forms).
- Auto-discovery of custom-field schemas. Custom-field gids must be declared in
bernstein.yamlby the operator.