Guide
Customer engagements
Status + milestones + sign-off + change-request comments — the engagement-shaped semantics threads lacked.
An engagement is a piece of work the agency is doing for one of
your tenants with customer sign-off. Threads grouped runs by
thread_id but lacked the engagement-shaped semantics a real
multi-day agency project needs: no sign-off, no milestone tracking,
no SOW alignment.
The engagement surface adds them as a first-class API. v0 ships the
wire surface; the customer-facing UI (/engagements,
/engagements/[slug]) follows.
#Shape
- Engagement —
slug,name,description,status(one ofactive,paused,complete,archived). Slug is unique per tenant. Settingstatus: archivedstampsarchivedAtso list filters can hide closed engagements without losing the row. - Milestone —
title,description,dueAt,status. Status flow:pending→signed_off(terminal) orrejected(terminal). Sign-off capturessignedOffBy(the user id from the JWT) andsignedOffAt(server timestamp). Reject capturesrejectedReason. Decisions are terminal: a rejected milestone cannot then be signed off — the agency must create a fresh milestone for re-review. This prevents the agency from silently re-signing work the customer already turned down. - Comment —
body,kind, optionalrunId. Kind is one of:comment— free-form discussionchange_request— a follow-up the agency must address before the next milestonearchitecture_decision— pinned rationale for a design choice Comments can reference a specific run (the architect's decision-log run, etc.) viarunId; omitting it makes the comment engagement-level.
Every row is tenant-scoped via WHERE tenant_id = $1; cross-tenant
access is impossible.
#API
GET /v1/engagements — list (?status=)
POST /v1/engagements — create (slug + name + description?)
GET /v1/engagements/:slug — fetch
PUT /v1/engagements/:slug — update name/description/status
GET /v1/engagements/:slug/milestones — list
POST /v1/engagements/:slug/milestones — create
POST /v1/engagements/:slug/milestones/:mid/sign-off
POST /v1/engagements/:slug/milestones/:mid/reject — body: { reason }
GET /v1/engagements/:slug/comments — list (?kind=)
POST /v1/engagements/:slug/comments — create (body, kind?, runId?)Errors:
- HTTP 409
engagement_slug_conflicton duplicate(tenant, slug). - HTTP 409
milestone_already_decidedwhen someone tries to sign-off-or-reject a terminal milestone. - HTTP 400 on invalid slug shape (must match
^[a-z0-9][a-z0-9-]*$) or unknown comment kind. - HTTP 404 on unknown slug or milestone id.
#Example flow
A friendly first-customer engagement looks like:
# 1. Create the engagement.
curl -X POST $ALDO_API/v1/engagements \
-H "Authorization: Bearer $ALDO_API_TOKEN" \
-H "content-type: application/json" \
-d '{"slug":"acme-q3","name":"ACME Q3 platform rebuild","description":"Lift-and-shift CRM to multi-tenant arch."}'
# 2. Create three milestones up front.
for title in "spec sign-off" "staging deployed" "go-live"; do
curl -X POST $ALDO_API/v1/engagements/acme-q3/milestones \
-H "Authorization: Bearer $ALDO_API_TOKEN" \
-H "content-type: application/json" \
-d "{\"title\":\"$title\"}"
done
# 3. Customer signs off on the spec milestone after reviewing.
curl -X POST $ALDO_API/v1/engagements/acme-q3/milestones/$MID/sign-off \
-H "Authorization: Bearer $ALDO_API_TOKEN"
# 4. Customer requests a change mid-sprint.
curl -X POST $ALDO_API/v1/engagements/acme-q3/comments \
-H "Authorization: Bearer $ALDO_API_TOKEN" \
-H "content-type: application/json" \
-d '{"body":"Switch the OAuth provider from Google to Okta","kind":"change_request"}'
# 5. Architect's decision pinned to the discussion.
curl -X POST $ALDO_API/v1/engagements/acme-q3/comments \
-H "Authorization: Bearer $ALDO_API_TOKEN" \
-H "content-type: application/json" \
-d '{"body":"Decision: Postgres + Hono. Reasoning in run xyz.","kind":"architecture_decision","runId":"xyz"}'#Combine with budget caps + Telegram
A real unsupervised engagement combines four primitives:
- Engagement with milestones + sign-off (this guide).
- Tenant budget cap —
PUT /v1/tenants/me/budget-capwith{ usdMax: 250, hardStop: true }so the run can't burn $250 overnight on a stuck loop. - Telegram integration subscribed to
approval_requestedso the customer approves destructive ops from a phone (see Integrations). - Hybrid CLI so local-only agents (privacy-tier sensitive)
stay on the customer's machine while cloud-tier agents delegate
to
ai.aldo.tech(see Hybrid CLI).