Quaspar Build — API Reference
The complete developer reference for building, testing, deploying, and operating healthcare AI agents on Quaspar Build.
Base URL — your workspace's API URL (shown in your dashboard under Settings → API). All paths below are relative to it and begin with /v1. An interactive OpenAPI explorer is available for your workspace at /docs.
Versioning & stability — the API is versioned by path (/v1). Backwards-incompatible changes ship under a new version; additive fields may appear at any time, so parse responses tolerantly.
Support — questions or issues: bhavesh@quaspar.com.
Content type — JSON in, JSON out (Content-Type: application/json), except file uploads (multipart) and SSE streams (text/event-stream).
1. Authentication
Two credential types. Everything except registration, login, health, and inbound provider webhooks requires one of them.
| Type | Header | Get it from | Use for |
|---|---|---|---|
| User token (JWT, 1h) | Authorization: Bearer eyJ… | POST /v1/auth/login / register | Humans, frontends |
| API key | Authorization: Bearer qsp_… | POST /v1/api-keys | Servers, Twilio/Retell webhooks (?key=qsp_…), CI, the Chrome extension |
Tokens expire after ~1 hour; exchange the refresh_token at POST /v1/auth/refresh. API keys don't expire (revoke via DELETE /v1/api-keys/{id}).
Roles (per organization): owner > admin > staff > viewer. Each endpoint below states its minimum role; unlabeled = any authenticated member. Every request is tenant-scoped: you can only ever see your own organization's data.
POST /v1/auth/register
Creates an organization ("practice") and its first user (role owner). Returns tokens immediately.
// request
{ "email": "amy@ortho.example", "password": "SomethingLong1", "full_name": "Amy Chen",
"organization_name": "Lakeview Orthopedics", "specialty": "orthopedic" }
specialty ∈ rheumatology | orthopedic | dermatology | cardiology | primary_care | other (tunes the compiler's healthcare context). Password: ≥10 chars with a letter and a number → 400 otherwise.
// response 200
{ "access_token": "eyJ…", "refresh_token": "eyJ…", "organization_id": "…", "user_id": "…", "mfa_required": false }
POST /v1/auth/login
{ "email", "password" } → same token shape. If the user enabled MFA, the response has "mfa_required": true and the access token is restricted: call POST /v1/auth/mfa/verify with { "code": "123456" } to receive full tokens. Repeated failures are throttled per email+IP (429).
POST /v1/auth/refresh
{ "refresh_token" } → new access+refresh pair. 401 if revoked/expired → send the user to login.
POST /v1/auth/mfa/enable → { "otpauth_uri": "otpauth://totp/…" } (render as QR)
POST /v1/auth/mfa/confirm — { "code" } → { "mfa_enabled": true }
POST /v1/auth/mfa/verify — { "code" } after a mfa_required login → full tokens
2. Conventions
Errors — always JSON:
{ "detail": "Agent is not LIVE" } // simple
{ "detail": { "error": "Could not apply that change", "reason": "…" } } // structured
{ "detail": "…", "request_id": "4f9a…", "error": "TypeError: …" } // unexpected 500s carry the cause
| Status | Meaning |
|---|---|
| 400 | Malformed input (message says which field) |
| 401 | Missing/expired credentials (refresh, then re-login) |
| 403 | Role too low for this action |
| 404 | Not found in your organization |
| 409 | State conflict — deploy gates, lifecycle transitions, BAA blocks. The detail is designed to be shown to users verbatim |
| 413 / 415 | Attachment too large / unsupported type |
| 422 | The model or validator rejected the request (detail.reason explains) |
| 429 | Org rate limit (300 req/min default) or login throttle |
| 502 | Upstream AI-provider failure; detail is a plain-English fix hint |
Every response carries X-Request-Id; unexpected errors are logged server-side under the same id.
SSE streams (/agents/build/stream/{id}, …/executions/{id}/stream, /approvals/stream, /agents/activity/stream, /browser/stream): standard Server-Sent Events. Do not use EventSource — it cannot send the Authorization header. Use fetch with a streaming reader; frames are separated by blank lines and may use \r\n; each data: line is one JSON event. The TypeScript client (frontend/quaspar-api.ts, method buildStreamAt) implements this correctly — copy it rather than re-deriving.
Timestamps are ISO-8601 UTC. Ids are UUID strings.
3. Quickstart: zero → running agent in nine calls
BASE=https://YOUR-SERVICE-URL/v1
# 1. Register (once)
TOK=$(curl -s $BASE/auth/register -H 'content-type: application/json' -d '{
"email":"dev@clinic.example","password":"SomethingLong1","full_name":"Dev",
"organization_name":"Dev Clinic","specialty":"orthopedic"}' | jq -r .access_token)
A="Authorization: Bearer $TOK"
# 2. Build an agent from plain English (the 4-pass compiler; 10–40s on a real model)
AGENT=$(curl -s $BASE/agents/build -H "$A" -H 'content-type: application/json' -d '{
"intent":"Manage incoming referrals, verify insurance, prepare scheduling, and notify staff if anything is missing"}')
AID=$(echo $AGENT | jq -r .id)
# 3. Evaluate it (simulated cases; required before LIVE)
curl -s $BASE/agents/$AID/test -X POST -H "$A" | jq '{state:.deployment_state, metrics:.metrics}'
# 4. Deploy
curl -s $BASE/agents/$AID/deploy -H "$A" -H 'content-type: application/json' -d '{"state":"LIVE"}' | jq .deployment_state
# 5. Run with a simulated trigger (fixture = fake world; no real systems touched unless connected)
RUN=$(curl -s $BASE/agents/$AID/run -H "$A" -H 'content-type: application/json' -d '{
"fixture":{"patient_ref":"SIM-1","insurance":{"active":true},"fields_missing":[],"appointment_type":"urgent"}}')
EX=$(echo $RUN | jq -r .execution_id)
# 6. Watch it (poll or SSE) — it pauses at WAITING_APPROVAL for the human gate
curl -s $BASE/agents/$AID/executions/$EX -H "$A" | jq .status
# 7. Approve
AP=$(curl -s "$BASE/approvals?status=PENDING" -H "$A" | jq -r '.[0].id')
curl -s $BASE/approvals/$AP/decide -H "$A" -H 'content-type: application/json' -d '{"decision":"APPROVED"}' | jq .
# 8. Confirm completion + read the event log
curl -s $BASE/agents/$AID/executions/$EX -H "$A" | jq '{status, events:[.events[].type]}'
# 9. Everything you just did is in the audit log
curl -s "$BASE/organizations/me/audit?limit=20" -H "$A" | jq '.[].action'
4. Core concepts
AgentSpec — the machine-readable artifact the compiler produces and the runtime executes. Code is never the source of truth; the spec is. Top-level fields:
name, purpose, agent_type (BACKGROUND_AGENT, INTERACTIVE_AGENT, VOICE_AGENT, CLINICAL_SUPPORT_AGENT, …), trigger ({kind: manual|schedule|webhook|referral_received|inbound_call|…}), workflow (nodes[] with id, name, kind ∈ detect|retrieve|verify|decide|prepare|act|approve|notify|escalate|complete, tool?, capability?, instructions?; edges[] with source, target, condition?), tools[] (key, name, capability, required_connection?), permissions[] (capability, autonomy, scope{}, rationale), autonomy{} map, instructions[], escalation_rules[], guardrails{}, evaluation{min_task_success,…}, voice{} (see §12), communication{}. Specs written by older platform versions are upgraded automatically on load — an old agent never breaks.
Capabilities & autonomy — permissions are granted per capability (ehr.read, insurance.read, insurance.submit, authorization.submit, scheduling.read, scheduling.write, patient.message, staff.message, document.draft, document.send, referral.read, referral.write, knowledge.read, browser.navigate, external.submit, …) at one of five levels: OBSERVE < RECOMMEND < DRAFT < APPROVAL_REQUIRED < AUTONOMOUS. A deterministic security gate (code, not the model) enforces: high-risk capabilities are AUTONOMOUS only when the user explicitly asked and the permission is scoped; org-blocked capabilities drop to OBSERVE; unused permissions are stripped (least privilege); clinical-advice instructions are removed.
Lifecycle — DRAFT → TESTING → READY → LIVE ⇄ PAUSED → ARCHIVED. Going LIVE requires (a) the specific version passed the evaluation gate and (b) the HIPAA/BAA compliance check. A LIVE agent serves its pinned live_version_id; edits create new draft versions that never reach production until independently tested and promoted. Runtime always enforces the PolicyEngine per action regardless of deployment state.
5. Agents
GET /v1/agents
List. → [{ id, name, agent_type, deployment_state, latest_version_id, live_version_id, created_at, updated_at }]
POST /v1/agents/build — the compiler, synchronous
{ "intent": "plain-English description", "name": "optional display name" }
Runs the 4-pass compile (understand → design → security → explain) and creates the agent + v1.0. → the full agent object incl. version.spec and version.design (analysis, security, explanation — the plain-language "how this agent works"). Errors: 422 when the request is unsafe/uncompilable (detail.reason), 502 with a fix hint when the AI provider fails.
POST /v1/agents/build/stream → GET /v1/agents/build/stream/{build_id} — the compiler, streamed
POST returns { build_id, stream_url } immediately; GET the stream_url (SSE) to watch. Event types (also the frontend's contract):
| type | payload highlights |
|---|---|
BUILD_STEP_STARTED / BUILD_STEP_COMPLETED | step ∈ understand, design, security, tools, permissions, explain |
INTENT_DETECTED | goal, trigger |
ASSUMPTION_MADE / QUESTION_RAISED / RISK_NOTED | assumption / question / severity, issue, fix |
NODE_CREATED / EDGE_CREATED | node{id,name,kind,tool} / edge{source,target} |
TOOL_ADDED / CONNECTION_REQUIRED | tool / connection, connected (amber when false) |
PERMISSION_ADDED | permission{capability, autonomy, scope} |
SECURITY_ENFORCED | what the deterministic gate changed |
DESIGN_EXPLAINED | explanation{summary, how_it_works[], what_needs_your_approval[], what_runs_automatically[], assumptions[], questions[], connections_needed[]} |
BUILD_COMPLETED | agent (full object), warnings[], follow_up (builder-chat only) |
BUILD_FAILED | error |
GET /v1/agents/{id}
Full object: { id, name, agent_type, deployment_state, live_version_id, version: { id, version, spec, design, change_summary, diff } } (version = latest).
POST /v1/agents/{id}/modify
{ "instruction": "Only schedule routine appointments automatically" }
Interprets the change against the current spec, applies a patch, re-runs the security gate, creates the next version (draft — the LIVE pin is untouched). If the model's patch produces an invalid spec, one automatic self-repair round-trip runs; if still invalid → 422 { error, reason }. → agent object + patch { summary, operations }, diff, warnings[].
POST /v1/agents/{id}/test
Runs the simulation evaluation on the latest version: synthetic cases through the real runtime with simulated tools. → { deployment_state, metrics: { task_success_rate, unauthorized_actions, cases, … } }. Passing (success ≥ spec's evaluation.min_task_success, default 0.9, and zero unauthorized actions) moves TESTING → READY and unlocks deploy for that version.
POST /v1/agents/{id}/deploy — role: admin
{ "state": "LIVE" | "PAUSED" | "ARCHIVED" | "TESTING", "version_id": "optional: promote a specific version" }
LIVE re-checks (1) the evaluation gate for that version → 409 { error, gate:{passed,checks,reason} }, (2) HIPAA/BAA compliance (model provider + PHI-carrying connectors incl. your signed-BAA attestation) → 409 { error, problems:[…] }. Both details are written to be shown to the user as a checklist. Success pins live_version_id.
POST /v1/agents/{id}/run
{ "trigger": { "any": "payload your workflow reads" }, "fixture": { "…simulated world…" } }
Queues an execution of the pinned live version (409 if not LIVE). In queue mode the job is picked up immediately (the API nudges the queue on enqueue and services it in-process when no dedicated worker is deployed) — poll the poll URL or open the execution's SSE stream; QUEUED should last well under a second. fixture seeds the simulated tools (e.g. patient_ref, insurance.active, phone) — use it for testing even in production; connected systems execute for real. → { execution_id, status: "QUEUED" } (queue mode) or the completed record (inline mode).
POST /v1/agents/{id}/test-scenarios
Agent-specific scenario testing. Derives scenarios from this agent's spec (missing information, inactive insurance, duplicates, prior auth, urgency — only the ones its tools/capabilities make meaningful), runs each through the real runtime with fully simulated tools (nothing external is touched even with connectors attached; runs record mode: "scenario"), and returns a detailed report:
{ "version": "1.1", "summary": "4/5 scenarios behaved (2 paused for approval as designed); 2 finding(s) to review.",
"coverage": { "nodes_total": 6, "nodes_visited": 6, "tools_total": 5, "tools_used": 4 },
"scenarios": [ { "name": "missing_information", "description": "…", "expect": "…", "fixture": { … },
"status": "WAITING_APPROVAL", "execution_id": "…", "path": ["intake", "verify", "notify"],
"actions": [ { "node": "verify", "policy": "ALLOW", "capability": "insurance.read" }, … ],
"approvals": ["…"], "escalations": [], "issues": [] } ],
"findings": ["Tools granted but never used in any scenario: send_fax — least-privilege says remove them…", …] }
Optional ?version_id= tests a specific version. Use it after every meaningful edit — the scenarios re-derive from the new spec.
POST /v1/agents/{id}/autonomy/accept — role: admin
Informed consent for unattended ("no human approval", 24/7) operation. When a build or modify explicitly requests full autonomy, the response includes autonomy_consent { required, message, capabilities[], risks{cap:[…]}, general[], non_negotiable[] } while the agent stays at APPROVAL_REQUIRED. Show the risks; on acceptance call this endpoint with { "capabilities": [...], "accept": true } → a new version with those capabilities AUTONOMOUS, the acknowledgment (who/when) recorded in the spec and audit log (agent.autonomy_accepted), and future edits and evaluations respect it. Org-blocked capabilities return 409 and can never be accepted; voice identity verification before PHI is never subject to this flow. Test + deploy the new version to make it live.
GET /v1/agents/{id}/executions?limit=50 → list { id, status, mode, agent_version_id, created_at, duration_ms }
GET /v1/agents/{id}/executions/{exId}
→ { id, status, mode, agent_version_id, result, visited[], events[] }. status ∈ QUEUED, RUNNING, WAITING_APPROVAL, WAITING_BROWSER, COMPLETED, FAILED, ESCALATED. Event types include EXECUTION_STARTED/COMPLETED/FAILED, NODE_STARTED/COMPLETED, TOOL_CALLED, POLICY_CHECK (decision), APPROVAL_REQUESTED/GRANTED/REJECTED, KNOWLEDGE_RETRIEVED (data.sources — which documents the agent relied on), ESCALATED. Reading an execution writes a phi.viewed audit row.
GET /v1/agents/{id}/executions/{exId}/stream — SSE of those events live.
Versions
GET /v1/agents/{id}/versions → [{ id, version, source (build|modify|restore), change_summary, created_at }] ·
GET /v1/agents/{id}/versions/{version_id} → full version ·
GET /v1/agents/{id}/versions/{a_id}/diff/{b_id} → { added[], removed[], changed[{path,from,to}], affected_nodes[], affected_permissions[] } ·
POST /v1/agents/{id}/restore { "version_id" } → copies an old version to a new latest draft (test + deploy to promote = rollback).
POST /v1/agents/{id}/feedback — { "text", "execution_id"?, "rating"? } → interpreted into improvement signals.
DELETE /v1/agents/{id} — role admin. Archives (audit history is never deleted).
GET /v1/agents/activity/stream — SSE firehose of execution events across the org (dashboards).
GET /v1/templates + POST /v1/agents/from-template/{template_id} — curated starting points.
6. The Builder conversation (chat-driven building)
The chat surface: every message the person types is routed by the backend into agent work. The frontend must never interpret text; it displays reply verbatim and branches on action.
POST /v1/builder/sessions → { id, agent_id: null } ·
GET /v1/builder/sessions → recent list ·
GET /v1/builder/sessions/{id} → { id, agent_id, messages[{role, content, meta, at}] }
POST /v1/builder/sessions/{id}/messages
{ "text": "whatever the person typed" }
→ { action, reply, session_id, agent_id, … } where action ∈
| action | meaning | extra fields | client behavior |
|---|---|---|---|
ask | too thin to design safely | — | show reply (a single question) |
build | designing a new agent | build_id, stream_url, intent | show reply, then open the SSE at stream_url (§5 events); BUILD_COMPLETED carries agent + follow_up |
modify / answer_q | change to this session's agent | agent (with new version), warnings[] | show reply, refresh panels from agent.version |
explain | question about the agent | — | show reply |
refuse | unsafe (e.g. clinical advice) | — | show reply styled as a refusal |
The router sees the whole conversation, the session's attachments, and the org's connected systems — an attached payer policy or a connected Twilio changes the design. Deterministic guards: no modify without an agent, no build from a <4-word message, unknown actions become ask.
7. Attachments (builder + agent chat sessions)
POST /v1/sessions/{session_id}/attachments — multipart file (+save_to_knowledge=true optionally). PDF/DOCX/TXT/CSV/MD/PNG/JPG/WEBP, ≤15 MB, ≤20 per session. Pipeline: extract (images transcribed by the org's vision-capable model) → structured analysis → encrypted cache (dedupe by content hash; raw bytes never stored). → { id, filename, kind, status, analysis: { document_type, summary, key_facts[], fields{}, phi_present, how_to_use }, chars }. Errors 415 / 413 / 422 with detail.error.
GET /v1/sessions/{session_id}/attachments list · DELETE /v1/sessions/{session_id}/attachments/{attachment_id} · POST /v1/sessions/{session_id}/attachments/{attachment_id}/save → { knowledge_source_id } (promotes to the practice knowledge base for runtime retrieval).
From upload onward, every model call in that session sees an ATTACHED DOCUMENTS block. Attachments are PHI: encrypted, audited, purged by retention with the session.
8. Approvals
GET /v1/approvals?status=PENDING|APPROVED|REJECTED|EXPIRED → [{ id, agent_id, agent_name, execution_id, action, reason, payload, risk, expires_at, created_at }]
POST /v1/approvals/{id}/decide
{ "decision": "APPROVED" | "REJECTED" | "EDITED", "note": "?", "edited_payload": { … when EDITED } }
→ { ok, execution: { id, status } }. Approving resumes the paused execution in the background (poll the execution or use its SSE). EDITED runs the action with your payload — the diff is audited. Expired approvals fail the execution safely.
GET /v1/approvals/stream — SSE (APPROVAL_REQUESTED events) for live inbox badges.
9. Knowledge
POST /v1/knowledge/upload — multipart file, title?, scope=practice|agent → chunked, embedded, INDEXED.
POST /v1/knowledge/instruction — { "text": "Our office requires MRI reports attached to every knee referral." } — one-line rules, immediately retrievable.
GET /v1/knowledge → sources incl. the shared payer library (library: true, org-wide read-only). · GET /v1/knowledge/stats → { sources, indexed, processing, corpus_bytes, library_sources } · GET /v1/knowledge/search?q=… → [{ title, text, score }] · DELETE /v1/knowledge/{source_id}.
At runtime, retrieval merges practice + library; run events show KNOWLEDGE_RETRIEVED with the exact sources relied on.
10. Connectors, models, EHR, tools
GET /v1/connectors/catalog
→ { categories: { ehr: [ … ], clearinghouse: [ … ], fax: [ … ], sms: [ … ], voice: [ … ], … }, connected: [keys] } — an object keyed by category name, each item { key, name, description, healthcare_notes, baa_available, config_fields[], secret_fields[], connected, baa_signed? }.
POST /v1/connectors/{key}/connect — role admin
{ "config": { "from_number": "+13125550100" }, "credentials": { "account_sid": "AC…", "auth_token": "…" }, "baa_signed": true }
Validates required fields (400 Missing: …), stores credentials in the platform secret store (never the DB), registers the connector's tools for the org, records your BAA attestation. From the next run, the connector's real implementation replaces simulation for exactly the tools it provides.
POST /v1/connectors/{key}/attest-baa — { "signed": true } — record a signed vendor BAA after the fact (audited). PHI-carrying agents cannot go LIVE over an un-attested connector (409 problems).
DELETE /v1/connectors/{key_or_connection_id} — revokes the stored secret, falls back to simulation.
GET /v1/connectors — org's connections. · GET /v1/tools — the tool catalog agents can use. · GET /v1/tools/connections — raw connection rows.
AI models
GET /v1/connectors/models/providers → { providers: [{ key, name, baa, needs[], default_model, notes, connected }], connections }. Providers without a BAA are visibly marked; the platform blocks PHI agents from running on them.
POST /v1/connectors/models/connect — { provider, api_key?, base_url?, model?, default? } (role admin). Org connections override the platform default per task.
EHR (SMART on FHIR)
POST /v1/connections/ehr/smart/begin — { vendor: epic|athena|ecw|cerner|smart_sandbox, fhir_base? } → { authorize_url, state }; open in a popup; the practice signs into their EHR (Quaspar is the registered app — no EHR passwords touch Quaspar). 409 if the vendor isn't platform-registered yet. The popup lands on GET /v1/connections/ehr/smart/callback; SPAs may instead call POST /v1/connections/ehr/smart/complete { state, code }. POST /v1/connections/ehr/backend-services for system-to-system JWKS setups. Once connected, FHIR-backed tools replace simulations.
POST /v1/connections/mcp + POST /v1/connections/mcp/{server_id}/rediscover and GET/POST /v1/tools/mcp — attach Model-Context-Protocol servers; their tools join the catalog.
11. Triggers: schedules & webhooks
GET/POST /v1/triggers/schedules — { agent_id, name, cron?("0 8 * * 1-5"), interval_seconds?, timezone?, payload? }; the scheduler runs LIVE agents on time. DELETE /v1/triggers/schedules/{id}. POST /v1/triggers/tick — manual sweep (ops/testing).
POST /v1/triggers/webhooks — { agent_id, name } → { id, url, secret } (secret shown once). External systems POST the returned URL (POST /v1/triggers/webhooks/{wid}/receive) with header X-Quaspar-Signature: sha256=<hex HMAC-SHA256(secret, raw_body)>; valid calls queue a run with the body as trigger. Invalid signature → 401; unknown/disabled → 404.
12. Voice
Enable in the builder ("make it a phone agent…") or via modify; spec.voice: { enabled, provider, greeting, verify_identity_before_phi, transfer_number, after_hours{}, voicemail, max_call_minutes, record_calls, post_call_workflow_agent_id }. Identity gate: PHI tools are deterministically blocked until the caller verifies (name + DOB) when verify_identity_before_phi is on.
- Twilio: point your number's A call comes in webhook (POST) at
POST /v1/voice/{agent_id}/twilio/voice?key=qsp_…. Status + voicemail callbacks:POST /v1/voice/{agent_id}/twilio/status,POST /v1/voice/{agent_id}/twilio/voicemail. - Retell (lowest latency): set the Retell agent's Custom LLM URL to
wss://HOST/v1/voice/{agent_id}/retell/ws/{call_id}?key=qsp_…. - Vapi / generic:
POST /v1/voice/{agent_id}/{provider}/webhook. - Outbound:
POST /v1/voice/{agent_id}/calls—{ to, reason?, expected_identity?{full_name,date_of_birth} }(agent must be LIVE, voice-enabled, telephony connected). - History:
GET /v1/voice/{agent_id}/calls→[{ id, direction, status, outcome, duration_seconds, turns, identity_verified, summary }];GET /v1/voice/{agent_id}/calls/{call_id}adds the fulltranscript[](reading it is audited asphi.viewed). After each call: model-written summary + optional follow-up workflow run.
13. Evaluations, datasets, rollouts
GET /v1/agents/{id}/evaluations — history with metrics · GET /v1/agents/{id}/evaluations/compare — versions side-by-side · GET /v1/agents/{id}/versions/{version_id}/gate — would this version pass? { passed, checks, reason }.
Datasets: GET /v1/agents/{id}/datasets and POST /v1/agents/{id}/datasets (cases: { fixture, expect }), POST /v1/agents/{id}/datasets/from-live (replay real executions as regression cases), POST /v1/agents/{id}/datasets/{dataset_id}/run (evaluate a version against it).
Rollouts: POST /v1/agents/{id}/rollouts { version_id, mode: canary|shadow, percent } — canary sends a traffic slice to the candidate; shadow runs it silently alongside for comparison; POST /v1/agents/{id}/rollouts/{rollout_id}/{action} with action promote or abort. GET /v1/agents/{id}/rollouts lists.
14. Organization, admin, observability
GET /v1/organizations/me · PATCH /v1/organizations/me (name, settings incl. blocked capabilities) · GET /v1/organizations/me/members · POST /v1/admin/members { email, role } (user must be registered) · GET /v1/organizations/me/usage — model spend/calls · GET /v1/organizations/me/audit?limit= — every consequential action { action, actor, agent_id, result, created_at } incl. phi.viewed rows (append-only, 7-year retention).
POST /v1/organizations/me/emergency-stop — role admin. Pauses every LIVE agent now; resuming is per-agent. → { paused, agents[] }.
Budgets: GET/POST /v1/admin/budgets { limit_usd, hard_stop, agent_id? } — hard stop pauses spend at the cap.
GET /v1/admin/export — full tenant export (owner). · GET /v1/admin/memories, POST /v1/admin/memories, DELETE /v1/admin/memories/{mid} — inspect/correct agent long-term memories. · /v1/admin/subscriptions — outbound event subscriptions ({ id, signing_secret }, signature as in §11).
GET /v1/observability/overview — { live_agents, executions_24h, approvals_pending, error_rate, p50_ms, … } · GET /v1/observability/executions/{exId}/trace — spans per node/tool/model call with timings and cost.
API keys: GET /v1/api-keys · POST /v1/api-keys { name } → { key: "qsp_…" } shown once · DELETE /v1/api-keys/{id}.
Marketplace: GET /v1/marketplace · POST /v1/marketplace/publish (share a de-identified spec) · POST /v1/marketplace/{lid}/install · POST /v1/marketplace/{lid}/rate.
Browser tasks (Chrome-extension handoff): GET /v1/browser/tasks → POST /v1/browser/tasks/claim → POST /v1/browser/tasks/{tid}/report { result, complete } resumes the paused execution; GET /v1/browser/stream pushes new tasks live.
Chat (staff ↔ interactive agents): GET /v1/agents/{id}/chat/sessions and POST /v1/agents/{id}/chat/sessions (POST → { id }; 409 problems = BAA check) · GET /v1/agents/{id}/chat/sessions/{session_id} history · POST /v1/agents/{id}/chat/sessions/{session_id}/messages { text } → { reply, actions[{tool,decision}], approvals[] }.
15. Workspaces & team invitations
A practice is a workspace. One person registers the practice; everyone else joins with a short code — no invitation emails, no admin adding people one at a time.
Registration response. POST /v1/auth/register and POST /v1/auth/login return the usual tokens plus workspace context: organization_id, organization_name, role, created_organization, and — for owners and admins only — join_code. A practice creator therefore receives the shareable code in the sign-up response itself, so the app can display it immediately ("Share this code with your team"); staff never receive it in any response. Organizations created before join codes existed are backfilled automatically at startup and on first admin access, so every workspace can always invite.
Registration (POST /v1/auth/register) now takes either:
organization_name(+ optionalspecialty) → creates a practice and issues its join code immediately; the registrant isowner; orjoin_code→ joins that practice asstaff, with no organization name needed.
Sending neither returns 400. An unknown, rotated or disabled code returns 404 with a message the person can act on. Codes look like QSP-7K4M2X (no confusable characters), and input is normalized — lowercase and a missing QSP- prefix both work, because people retype these from a sticky note. Sign-in always lands the user in the workspace they belong to; a user in no workspace gets 409 with instructions rather than a broken session.
Roles. Four, in order: owner (created the practice; can transfer or remove ownership), admin (manages people and roles, connects integrations, deploys agents, handles billing), editor (creates, edits, tests and chats with agents, uploads knowledge, handles approvals — but cannot deploy to production, connect systems, manage people or touch billing), viewer (reads everything, changes nothing). staff is the legacy name for editor and is still accepted, and any unrecognized role normalizes to viewer rather than passing. GET /v1/workspace returns a roles[] catalog with each role's description and capabilities for the UI's picker.
What a join code hands out is the practice's choice: POST /v1/workspace/default-join-role {role} (admin) sets whether people who sign up with the code become admin, editor (the default) or viewer. A code can never grant ownership. The current value comes back on GET /v1/workspace as default_join_role, alongside assignable_roles — the roles the current user may hand out (owners can assign any role; admins can assign admin/editor/viewer but never owner, and cannot change an owner's role; the last owner is always protected).
GET /v1/workspace/code/{code} — public: validates a code and returns {valid, organization_name, message} so the sign-up form can show "You'll join Lakeview Orthopedics" before the account is created. Joining needs only name, email, password and the code — the workspace comes from the code itself, and blank strings for unused fields are treated as absent.
GET /v1/workspace — the switcher payload: this workspace, my_role, members[], and every workspace the user belongs to. Owners/admins also see join_code and join_code_enabled; staff never do, so the code can't be reshared by someone who shouldn't.
GET /v1/workspace/join-code (admin) — the code plus a ready-to-send share line.
POST /v1/workspace/join-code/rotate (admin) — invalidates the old code instantly (use when someone leaves).
POST /v1/workspace/join-code/enabled {enabled} (admin) — close or reopen joining entirely.
POST /v1/workspace/invite {emails[], note?, role_label?, app_url?} (admin) — sends the branded invitation email (HTML + plain text, with the workspace code) through the practice's own connected email channel: SendGrid → Microsoft 365 → Gmail. If none is connected it still succeeds, returning the rendered email.subject / email.text / email.html to copy — inviting a colleague is never blocked on an integration. Invalid addresses are rejected before anything sends; up to 25 recipients per call.
POST /v1/workspace/invite/preview {note?, app_url?} (admin) — renders the same email without sending, for a preview pane.
POST /v1/workspace/join {join_code} — join another practice while signed in (a physician covering two clinics). The joined workspace's plan member limit applies (402 if full).
POST /v1/workspace/switch {organization_id} — returns a fresh token scoped to that workspace; the frontend replaces its stored token and reloads.
POST /v1/workspace/leave — leave this workspace; returns the remaining ones. The last owner cannot leave (409) — a practice is never left without an administrator.
DELETE /v1/workspace/members/{user_id} (admin) — remove someone; they keep their login and lose only this workspace.
POST /v1/workspace/members/{user_id}/role {role} (owner) — owner/admin/staff/viewer. Demoting the last owner is refused.
Everything is audited: workspace.member_joined, workspace.member_left, workspace.member_removed, workspace.role_changed, workspace.join_code_rotated.
16. Research, imaging, genomics & clinical-data connectors
Agents can now work in medicine and research, not only the front office. New capabilities: research.read, genomics.read, drug.read, medication.read, imaging.read, lab.read, device.read, model.infer — all flowing through the same policy engine, approval gates and audit log.
Literature & trials (public, no credentials): PubMed/NCBI, ClinicalTrials.gov, Europe PMC, OpenAlex, Crossref, Semantic Scholar, bioRxiv/medRxiv. Results are normalized to {title, abstract, year, citations, source} so agents can compare and cite.
Genomics (public): ClinVar, Ensembl (incl. VEP consequence prediction), dbSNP, gnomAD population frequencies, UniProt, cBioPortal.
Drug discovery (public): PubChem, ChEMBL, Open Targets (target–disease association and known drugs), RCSB PDB, AlphaFold (with pLDDT confidence), STRING. DrugBank is licence-gated.
Medications: RxNorm/RxNav normalization and related concepts, openFDA labels and adverse-event counts.
Imaging: dicomweb connects any DICOMweb PACS/VNA — QIDO-RS study/series search and WADO-RS rendered retrieve, which returns a JPEG ready for a vision model. Google Cloud Healthcare DICOM is supported as a BAA-covered store.
Labs: FHIR Observation results with reference ranges and abnormal flags, a labs.trend tool that returns a time series plus direction, an HL7 v2 parser for ORU/ADT feeds, and LOINC code search.
Research data: REDCap export and data dictionary; OMOP CDM concept search and count-only cohort queries — cohorts under 11 patients are suppressed, by convention, to protect re-identification risk.
Wearables & remote monitoring: Fitbit, Dexcom CGM (with hypo/hyper counts), Withings; Apple Health / Google Health Connect via an authenticated upload endpoint from your own app.
GET /v1/model-router/available — every model endpoint this workspace has connected, with modality, task and where it runs.
POST /v1/model-router/route {modality?, task?, prefer_connection_id?, contains_phi?} — "analyze with my model" (pass prefer_connection_id) or "find the best compatible model" (pass modality/task). Ranking prefers self-hosted, then BAA-covered cloud, then public, and refuses a public endpoint outright when contains_phi is true, returning safe alternatives. Every route explains itself: chosen, reason, why[], alternatives[].
Every clinical tool returns a caution field the agent is expected to carry into its output — model results are drafts for a clinician, lab values are never interpreted to a patient, variant interpretation belongs to a genetic counselor.
17. Situational awareness & custom scenario testing
Every execution begins with a situation read of the incoming trigger: category (administrative / clinical / emergency / unclear), patient sentiment, priority, whether this is a repeat contact, and any emergency red flags. It is deterministic (no model call), attached to the execution state, and available to the workflow as situation context.
Two behaviours follow from it. Nothing clinical closes silently: if a run would finish with no human involved but the situation contains clinical or emergency content, the agent notifies staff and finishes as ESCALATED — the staff note names the category, the patient's tone, repeat contact, and any red-flag phrases. Testing reports what would happen:
POST /v1/agents/{id}/test-scenario { scenario, version_id? } — describe a real situation in plain words. The scenario is turned into a fixture, run through the real runtime with fully simulated tools, and returned as: verdict (handled appropriately / with concerns / not handled appropriately), narrative[] (plain sentences describing what would happen), situation (the read), what_happened (status, path, ordered timeline, messages_it_would_send each tagged patient- or staff-facing, approvals, escalations, blocked), and assessment (working_well, concerns, recommendations phrased as instructions to paste into the Edit tab).
POST /v1/agents/{id}/test-scenarios (derived scenarios) also now carries a per-scenario situation and flags any scenario where clinical content completed with no human involvement.
Execution events are strictly ordered by a monotonic seq across nodes and across resumes, so timelines read in the order things actually happened.
18. One-click OAuth (Google & Microsoft connectors)
GET /v1/oauth/providers — which OAuth families are configured and which connectors are one-click. POST /v1/oauth/{connector}/start { return_to? } (admin) → { url }: redirect the browser to the vendor's consent screen. GET /v1/oauth/callback — the signed-state callback: exchanges the code, stores long-lived credentials (access tokens are refreshed automatically before every use), creates the connection, and redirects back to the app. Covered connectors: gmail, gdrive, gsheets, google_calendar (Google) and outlook, onedrive, outlook_calendar, ms_bookings (Microsoft). Pasting an access token manually remains supported for all of them.
19. Connector catalog (expanded)
GET /v1/connectors lists the full catalog grouped by category; POST /v1/connectors/{key}/connect activates one with { config, credentials, baa_signed? }. Connectors implement existing capabilities — connecting one makes every agent's matching tools real, through the same policy/approval path, with no spec edits. Categories now include:
Communication — Gmail, Outlook/Microsoft 365, Slack, Microsoft Teams, Twilio (SMS+voice), WhatsApp (via Twilio), Zoom, SendGrid, RingCentral, Dialpad. Files & knowledge — Google Drive & Docs, OneDrive/SharePoint, Dropbox, Box, Notion, Confluence (bind retrieve_knowledge, so "train my agent on our SOPs folder" works by connecting the drive; PDF/Word/CSV/Excel uploads are handled natively by the knowledge base). AI models (/v1/models endpoints; per-agent choice in the spec's model field, PHI-compliance enforced per provider) — Claude, OpenAI, Gemini, Mistral, Cohere, Llama (Groq/Together/Fireworks), Azure OpenAI, AWS Bedrock, DeepSeek, xAI, OpenRouter, self-hosted Ollama/vLLM. Voice & speech — Twilio, Retell, Vapi, ElevenLabs (TTS), Deepgram + AssemblyAI (STT), RingCentral, Dialpad, Amazon Connect (credential-stored). Scheduling — Google Calendar, Outlook Calendar, Calendly, Cal.com, Acuity, Microsoft Bookings. EHR (SMART on FHIR R4) — Epic, athenahealth, eClinicalWorks, Oracle Health, NextGen, Tebra, DrChrono, AdvancedMD, Practice Fusion, ModMed, plus the free SMART sandbox. Insurance — Stedi (270/271/276/277), Availity, Claim.MD, Optum/Change, Waystar & Experian Health (partner-gated), CoverMyMeds, Surescripts. Automation — Zapier, Make, n8n, inbound/outbound webhooks, Custom REST, Custom GraphQL, OpenAPI Import (point at a spec URL; operations become agent tools automatically), MCP servers. Databases — Supabase, PostgreSQL (SELECT-only unless writes enabled), Firebase/Firestore, Airtable, Google Sheets. CRM & work — Salesforce, HubSpot, Pipedrive, Zoho, Monday, Asana, ClickUp, Jira.
20. Free trial, plans & billing (Stripe)
Every new practice starts on the free trial, created automatically at registration: 1 deployed agent, 3 draft agents, 100 agent executions, 30 days. GET /v1/billing/usage reports it as plan.key = "free" plus a trial object (is_trial, expired, ends_at, days_left) and a data_policy line. Warnings appear in the last 7 days and after expiry.
Clinical systems of record are closed on the trial. The free plan carries phi_allowed: false. Connecting one of these is the act of exposing a patient database, so they're refused with 402 and detail.limit = "phi_connector": EHRs, imaging (DICOM/PACS), lab feeds (HL7 v2, FHIR results), clearinghouses and payers, e-prescribing, fax, and research warehouses (REDCap, OMOP). The EHR SMART launch is gated at its first step. Two deliberate exceptions: LOINC (a code dictionary — no patients) and the SMART Health IT sandbox (synthetic patients by design), so a trial can demo an entire EHR workflow.
Everything else connects normally — messaging (Twilio, Gmail, Outlook, Slack, Teams), calendars, files, databases, CRM, automation, research and genomics reference data, model endpoints, and wearables (a person's own Fitbit or CGM is data they own, not a covered entity's records). Connecting a channel that could carry PHI returns a notice field reminding the user the trial isn't for real patient information, and records connector.free_plan_notice in the audit log. Unconnected tools still resolve through the simulated registry, so agents run end to end regardless.
When the 30 days end, new work stops: POST /v1/agents/{id}/run and POST /v1/agents/{id}/deploy return 402 with detail.limit = "trial_expired". Nothing is deleted — agents, versions, history and audit remain readable, so upgrading restores service instantly.
Upgrading (Stripe checkout → webhook or /billing/confirm) sets the plan, clears trial_ends_at, re-anchors the billing period, and unlocks PHI connectors and the new limits in the same transaction.
Creating a practice requires a work email. POST /v1/auth/register with organization_name rejects consumer and disposable mailboxes (400 with guidance). Staff joining an existing workspace with a join_code may use any address — front-desk hires often have no clinic mailbox.
Stripe lives in the backend; the frontend renders plans, redirects to Checkout URLs this API creates, and shows usage. Subscription truth arrives only via the signature-verified webhook. Configure STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET; without them, plans/usage work and checkout returns 503.
GET /v1/billing/plans — public plan config (from the plans table, editable without a deploy): key, name, monthly/yearly prices, all limits (-1 = unlimited), flags.
GET /v1/billing/usage (alias GET /v1/billing/subscription) — the dashboard payload: plan, period {start,end} (anchored to the subscribe date, rolling monthly), executions {used, limit, extra_credits, available, percent, display}, deployed_agents, draft_agents, team_members, integrations, warnings[] (70/80/90/100% messages), credit_packs[]. Internal model/infra costs are tracked in usage_events but never appear here.
POST /v1/billing/checkout { plan_key, interval: month|year, success_url?, cancel_url? } (admin) → { url } — redirect the browser there.
POST /v1/billing/credits/checkout { pack_id } (admin) → { url } — one-time execution-credit packs (500/$15, 2,000/$49, 10,000/$199); credits persist until used.
POST /v1/billing/portal (admin) → { url } — Stripe customer portal (payment method, invoices, cancel; cancellation is never hidden).
POST /v1/billing/confirm { session_id } — instant post-payment sync. Checkout success URLs carry ?session_id={CHECKOUT_SESSION_ID}; the frontend posts it here on landing. The backend fetches the session from Stripe (never trusting the browser), applies it if paid, and returns { applied, what, usage } — so the UI reflects the new plan/credits immediately instead of waiting for the webhook. Idempotent with the webhook: the same session applies exactly once. Subscribing (or upgrading) re-anchors the billing period to today, so the monthly allowance starts fresh; purchased credit packs persist until used and are unaffected by period rollover.
POST /v1/billing/webhook — Stripe events (signature-verified): checkout.session.completed (subscription → plan set + period re-anchored to today; payment with credits metadata → credits added), invoice.paid (each recurring renewal → status active; the monthly execution allowance rolls automatically from the anchored period, so a fresh month's executions appear without any action), invoice.payment_failed (status past_due — access continues as a grace state; enforcements still apply), customer.subscription.updated/deleted (cancel → graceful landing on Basic). Configure the Stripe endpoint with all five event types.
Live counters after every action. Every state-changing response — POST /v1/agents/build, POST /v1/agents/{id}/deploy, POST /v1/agents/{id}/run, POST /v1/connectors/{key}/connect and disconnect, POST /v1/admin/members, POST /v1/billing/confirm — includes a fresh "usage" object (same shape as GET /v1/billing/usage). Frontends should update their counters from it instead of issuing a follow-up fetch; a 402 anywhere means a limit was hit and carries the upgrade options.
Metering. One completed production run = 1 execution credit; unusually heavy runs cost more (+1 credit per 25 tool calls beyond the first 25). Draft/testing/eval/scenario runs are free; paused agents consume nothing. Enforcement returns 402 with detail { error, limit, plan, upgrade_to, options: [upgrade_plan, buy_credits?] } at: POST /v1/agents/build (draft limit), POST /v1/agents/{id}/deploy (deployed-agent limit, only when promoting a new agent), POST /v1/agents/{id}/run + inbound webhooks (execution credits; schedules skip + audit instead of failing), POST /v1/admin/members (team members), POST /v1/connectors/{key}/connect (integrations).
21. Health & readiness
GET /health — no auth. Config truth: provider, db, run mode, production_checks PASS/FIX list. GET /health?verify_ai=true makes one real model call → ai_check { ok, error?, hint? } — the first thing to check when builds fail. GET /ready — { ready: true } once the database answers; use as the deployment probe.
22. Testing your agents: the intended workflow
- Build conversationally, attach the payer policy / SOP the agent must follow, and read the
DESIGN_EXPLAINEDpanel — it is the contract. /testearly and often — simulated cases through the real runtime; the gate demands zero unauthorized actions, so permission problems surface here, not in production./runwith fixtures — deterministic fake worlds (insurance.active:false,fields_missing:["mri_report"], missing phone …) to walk every branch, including approval pauses and escalations. Fixtures are safe even on production deployments: only connected systems act for real.- Build a regression dataset (
datasets/from-live) once real traffic exists; run it against every candidate version; promote via canary. - Read the audit log as a developer tool — every policy decision, approval, and PHI view is a row; if you can't explain a row, that's the bug.
23. Adaptive learning: experiences & the neural net
Every agent gets smarter with use, through three layers that build on each other:
- Episodic memory. Every finished live execution is recorded as an experience — the situation (PHI-redacted before storage), the path the agent took, and the outcome (
COMPLETED/ESCALATED/FAILED) — embedded with pgvector. - Experience recall. When a similar situation arrives, the engine retrieves the most similar past runs and tells the agent what worked and what failed last time (a "Learned from experience" context block). Works from the very first recorded run; visible in the execution timeline as
EXPERIENCE_RECALLED. - A per-agent neural network. A small MLP trains incrementally on accumulated outcomes (experience replay, weights persisted per agent) and predicts success probability for new situations. It starts in shadow mode — predictions logged (
NEURAL_PREDICTIONevents withmode: "shadow") but never used — and only graduates to active once it has 40+ experiences AND beats the majority-class baseline on held-out data by ≥5 points. Graduation emitsNEURAL_NET_ACTIVATED. A net trained on a handful of examples is confidently wrong; shadow gating is why this system can be trusted in a clinic.
Consolidation ("sleep" phase). Every 10th experience, clusters of 3+ repeated successes on the same kind of situation are distilled into durable procedure memories (the same AgentMemory the runtime already injects into prompts), emitting PROCEDURES_CONSOLIDATED. Human feedback raises a procedure's confidence.
Human feedback is the strongest teacher. A staff verdict on a past run overrides the automatic outcome label and retrains the net immediately.
Learning is best-effort by design: a failure anywhere in this layer can never fail an execution. Experience text passes the same redaction guardrail as agent memories — the pattern is stored, never the patient. Test/eval/scenario runs are not learned from (live mode only).
Endpoints
GET /v1/agents/{agent_id}/learning — the learning dashboard: experiences {total, by_outcome}, procedures_learned, and neural_net {mode: active|shadow, trained_on, holdout_accuracy, baseline, metrics}.
GET /v1/agents/{agent_id}/learning/experiences?limit=25 — recent experiences (id, execution_id, redacted situation, action path, status, reward, feedback_score, consolidated).
POST /v1/agents/{agent_id}/learning/feedback { execution_id, score } (owner/admin/editor) — teach the agent: score is −1 (wrong) to 1 (right). Overrides the automatic reward for that run, retrains the net, and returns its new state. 404 if no experience exists for that execution (e.g. a test run).
POST /v1/agents/{agent_id}/learning/train (owner/admin) — force a retrain now; returns mode, trained_on, holdout_accuracy, baseline, metrics.
POST /v1/agents/{agent_id}/learning/consolidate (owner/admin/editor) — run the sleep phase now; returns the procedures created.
Frontend guidance
Show a per-agent Learning panel from GET .../learning: experience count, outcome breakdown, procedures learned, and the net's status — render shadow mode as "still learning — needs N more experiences" (the metrics.reason string says exactly that) and active mode with its holdout accuracy vs baseline. Put a 👍/👎 on each execution row wired to the feedback endpoint; that one control is the single highest-leverage way a practice makes its agents smarter. Timeline events to render: EXPERIENCE_RECALLED, NEURAL_PREDICTION, NEURAL_NET_ACTIVATED, PROCEDURES_CONSOLIDATED, MEMORY_STORED.
Migration
c1e9a4b77d02 adds agent_experiences and neural_net_states (new tables, server defaults throughout). Deploy as usual; it applies on startup.
24. Performance & security hardening (v2)
Caching (Redis). GET /v1/billing/plans (5-min TTL) and the connector catalog shape (10-min TTL) are served from cache — Redis-shared across replicas when REDIS_URL is set, in-process otherwise, and a Redis outage silently falls back to the database. HIPAA rule enforced in code: only PHI-free data (plan config, catalogs) is ever cached; per-org connected flags are computed live on a deep copy so they never leak between organizations.
Background jobs. Invitation emails now go through the durable job queue (kind: send_email) whenever RUN_MODE=queue or WORKER_ENABLED=true — the invite endpoint returns instantly with {queued: true} per recipient and the worker sends with automatic retries. Agent runs were already queued; nothing user-facing blocks on SMTP, SendGrid, or model calls.
Database. Migration d4f2b8c31a55 adds ten composite indexes matched to the hottest queries (executions per agent, event replay by seq, worker claim scan, chat history, audit page, billing counters, learning replay buffer, approvals inbox). Models declare the same indexes, so the schema-parity test keeps them in lockstep.
Uploads. In addition to the existing type allowlist and 15 MB cap: magic-byte sniffing rejects any file whose bytes don't match its declared type (a script renamed to .pdf, HTML disguised as .txt). Raw upload bytes were already never stored — only extracted text — so uploads cannot be served back or executed, ever.
XSS defense in depth. All user-supplied display strings (names, org names, free-text feedback) are HTML-escaped and stripped of control/RTL-override characters at write time via app/core/sanitize.py, before they can reach any email, PDF, or page. Responses carry a strict Content-Security-Policy (script-src 'self', frame-ancestors 'none', object-src 'none'), Permissions-Policy, plus the existing nosniff/DENY/HSTS headers. Login is IP-throttled; JWTs are short-lived; sessions are stateless.
Webhooks. Already verified end-to-end: Stripe events require a valid Stripe-Signature (timestamp-tolerant HMAC) and inbound agent webhooks require X-Quaspar-Signature: sha256=HMAC(secret, raw body) with a per-webhook secret shown once.
Race safety. Concurrent first requests to /v1/billing/usage no longer 500 on the lazy subscription create (losers of the unique-insert race re-read the winner's row) — found and fixed by the load test.
Load testing. scripts/load_test.py --base https://staging-url --users 50 --seconds 60 simulates concurrent staff with a realistic read/write mix and reports p50/p95/p99 per endpoint; exits non-zero above a 1% error budget so it can gate deploys. An in-suite version (60 concurrent requests, zero 5xx) runs with every pytest.
Horizontal scaling readiness. Stateless auth (JWT), Redis pub/sub SSE bus, Redis-backed rate limiting, and a FOR UPDATE SKIP LOCKED job queue mean any number of Cloud Run instances can serve traffic and any number of workers can drain jobs without coordination.
Redis over TLS (Memorystore in-transit encryption)
All Redis clients (cache, rate limits, SSE bus) go through one TLS-aware factory. For a Memorystore instance with in-transit encryption and AUTH:
REDIS_URL=rediss://default:<AUTH_STRING>@<instance-ip>:6378— noterediss://(double s) and port 6378, not 6379.REDIS_CA_CERT=<PEM text>— the instance's CA certificate from "Download certificate" on the Memorystore details page. Store it in Secret Manager and mount it as an env var.- The Cloud Run service must have the Serverless VPC Access connector (or Direct VPC egress) attached — Memorystore is private-network only.
Plain redis://...:6379 URLs continue to work for TLS-off instances and local dev. Certificate verification is never disabled.
25. Exception intelligence (healthcare workflows that recover)
When a live workflow encounters something unexpected, Quaspar figures out what happened, determines what should happen next, gathers the missing information, and deploys the right agents to resolve it. Every live run that ends FAILED or ESCALATED becomes a diagnosed WorkflowException: deterministic classification (tool_failure | missing_data | permission_denied | guardrail_blocked | budget_exceeded | unexpected_content | delegate_failed | unknown), a diagnosis enriched by the agent's learned neural net and similar past runs, and a decision-table recovery plan (retry transient failures with backoff → gather missing information → deploy the best other LIVE agent, scored by capability overlap → escalate to a human after 2 attempts, always). The scheduler processes open exceptions every tick.
GET /v1/exceptions?status= — list · GET /v1/exceptions/{id} — diagnosis + plan + recovery state · POST /v1/exceptions/{id}/recover (owner/admin/editor) — run the next recovery step now · POST /v1/exceptions/{id}/resolve — a human closes it · POST /v1/exceptions/process (owner/admin) — process all open now · GET /v1/exceptions/agents/{agent_id}/risk — predicted failure risk for an agent (exception history + the learned net). GET /v1/agents/{agent_id}/brain — the agent's full cognition loop (perceive → recall → predict → act → reflect → recover).
26. Autonomous Research Programs (the research OS)
POST /v1/research/programs { objective, title?, config? } creates a persistent Research Program: eight specialized agents (Research Director, Literature, Biology, Data Analysis, Hypothesis, Experiment Design, Scientific Critic, Research Memory) that keep working toward the objective until paused. Agents wake on events (new_paper, dataset_uploaded, experiment_result, contradiction_found, new_hypothesis, research_gap, inactivity) and scheduled Director reviews; they create tasks for each other through a durable queue — no busy-polling. Digital research is autonomous; expensive compute, protocol changes, external submissions and material orders wait for approval (AWAITING_APPROVAL); physical lab and clinical actions are recorded as recommendations only (HUMAN_ONLY).
GET /v1/research/programs · GET /v1/research/programs/{id} (dashboard card: agents, evidence/hypothesis/experiment counts, open questions, last + next activity) · POST .../pause | /resume | /complete · POST .../events { event, payload } — external wake-ups · POST .../advance?steps=N — drive the loop synchronously (demos/tests) · GET .../tasks + POST .../tasks/{tid}/approval { approved, note } · GET/POST .../memory (?kind=hypothesis|evidence|paper|...; every item carries provenance: established_evidence | experimental_result | ai_inference | hypothesis | speculation — AI hypotheses are never presented as facts) · GET .../timeline — how the project evolved · GET .../canvas — the research graph (objective → hypotheses → evidence → experiments → results → conclusions → next actions) · GET /v1/research/roles.
27. The universal workspace (AI workforce platform)
Quaspar is a workspace where clinics, hospitals, labs, universities, biotechs, pharma companies, CROs and individual professionals deploy persistent AI workers alongside humans.
GET /v1/hub — the whole workspace in one call: workers (total, live, recent), teams, workflows, projects, research (active programs), tasks (open, waiting_review, mine), knowledge, data, integrations, activity (executions 24h, approvals pending, exceptions open, recent audit) — each section a dashboard panel.
AI workers & the hiring catalog
GET /v1/workers — the roster with live workload (open tasks, active executions, working/available). GET /v1/workers/catalog?category=healthcare|research|pharma|coordination — 30 curated archetypes (Prior Authorization, Referral, Patient Communication, Clinical Documentation, Eligibility, Scheduling, Medical Records, Coding Review, Clinical Research, Healthcare Data Analyst, QA; Literature, Research Analyst, Hypothesis, Data Scientist, Bioinformatics, Chemistry, Biology, Experiment Design, Scientific Critic, Research Director; Drug Discovery, Target Research, Compound Analysis, Clinical Trial Intelligence, Regulatory Research, Safety Analysis, Pharmacovigilance, Competitive Intelligence; Operations Director, Reporting). Every catalog entry also carries connector recommendations: connectors: [{need, why, options[], connected, connected_via}] — each need (e.g. "EHR access", "Literature sources") lists interchangeable providers, resolved live against the org's connected systems, plus a per-worker ready flag. The hire response repeats this as connectors and missing_connectors, so the UI can offer the right connections at the moment they matter. Workers still function without them (desk work + whatever IS connected) — recommendations are about unlocking full capability, never a gate on hiring. POST /v1/workers/from-archetype/{key} { name?, extra_intent? } — hires one: the archetype's intent runs through the full 4-pass compiler against your org's connected systems, so the same archetype is Epic-aware in one workspace and REDCap-aware in another. Custom private agents: POST /v1/agents/build as always.
Agent teams
POST /v1/teams/build { intent: "Build me an AI team for clinical trial operations" } — the model designs the composition from the catalog (deterministic fallback; unknown roles are dropped), every member is hired through the compiler, a shared project is created, kickoff tasks are seeded per specialist, and the director's synthesis task depends on all of them. GET /v1/teams · GET /v1/teams/{id} (members, task progress) · POST /v1/teams/{id}/status { ACTIVE|PAUSED|ARCHIVED } (pauses the project too) · POST /v1/teams/{id}/kickoff — dispatch pending team work now.
Projects & universal tasks (humans + AI in one system)
POST /v1/projects, GET /v1/projects, GET /v1/projects/{id} (tasks + team + shared memory), POST /v1/projects/{id}/status.
POST /v1/tasks { title, instructions?, project_id?, assignee_type: ai|human, assignee_id?, depends_on?: [task_id], condition?: {path, op: eq|ne|gt|lt|contains|truthy, value}, priority?, payload?, due_at? } — the universal unit of work. AI tasks on a LIVE agent run its real workflow (policy engine, approvals, audit; the task settles when the run finishes); otherwise the agent does desk work — an analysis-only reasoning pass over the task, project context and org knowledge, labeled ai_inference, no tools touched. Human tasks appear in the inbox (GET /v1/tasks?mine=true). When a task finishes, dependents whose conditions hold become READY automatically and dependents on the untaken branch are SKIPPED — that's the human→AI→human loop: Dr. Smith completes the physical experiment task, and the dependent AI analysis resumes on its own.
GET /v1/tasks?status=&assignee_type=&project_id=&mine= · GET /v1/tasks/{id} · POST /v1/tasks/{id}/assign | /complete { result, note? } | /cancel | /dispatch.
Visual workflow builder (no code)
Graph node types: trigger | agent | tool | decision | approval | human | action; decision branches carry conditions evaluated against the upstream step's result. POST /v1/workflows { name, graph } (returns validation), POST /v1/workflows/generate { description } — draft a valid graph from plain language, PUT /v1/workflows/{id} (graph edits bump the version; activating a broken graph is refused with the problems), POST /v1/workflows/{id}/validate, POST /v1/workflows/{id}/run { payload?, agent_id? } — instantiates the graph as a project of chained tasks (trigger/decision are structure, not tasks) and returns them, GET /v1/workflows/{id}/runs — run history with task progress.
Organizational memory
Completed work, decisions, deployed teams and workflow runs are remembered automatically (PHI-redacted — the pattern, never the patient). POST /v1/org-memory — record institutional knowledge explicitly · GET /v1/org-memory?kind= · GET /v1/org-memory/search?q= — semantic search across org memory + agent experiences + research memory · POST /v1/org-memory/ask { question: "Have we dealt with this before?" } → { answer, sources[], confidence }.
Per-agent access control
GET /v1/agents/{id}/access — the permission matrix: every capability with its autonomy level (autonomous | approval_required | restricted), tools, knowledge scopes, and consented-autonomous capabilities. POST /v1/agents/{id}/access (owner/admin) { mode: approval_required|restricted, capabilities?: [...], block?: [...] } — tightens permissions (creates a new draft version; test + deploy to promote). This endpoint can only tighten — raising to full autonomy always goes through the informed-consent flow (POST /v1/agents/{id}/autonomy/accept).
Analytics
GET /v1/analytics/overview?days=30 — executions by status with success rate, tasks done by AI vs humans, top workers, approvals and exceptions in the period, and estimated hours reclaimed.
Continuous work
Everything above is event-driven: AI tasks dispatch on creation or when dependencies settle (durable job queue in RUN_MODE=queue, inline otherwise); the scheduler sweep reconciles live runs into their tasks and wakes anything whose upstream settled; teams and workflow runs ride the same mechanism. Agents execute only when there is useful work.
Migration a9d3e71c4f20 adds projects, work_tasks, agent_teams, org_memory_items, workflow_defs (new tables, server defaults throughout). Deploy as usual; it applies on startup.
28. The request interpreter (free text → the right action)
Nobody types "build me an AI clinic agent." They type "i need something that keeps an eye on our prior auths and lets us know what paperwork is missing." POST /v1/interpret is the front door for every free-text sentence — send whatever the person typed and get back what they're actually asking for.
// POST /v1/interpret { "text": "...", "execute": false }
{ "understanding": {
"action": "build_worker", // build_worker | build_team | research_program | create_workflow | create_task | ask_memory | unclear
"goal": "the request, as typed",
"why": ["asks for one standing capability (matched the worker catalog)"],
"entities": ["prior auth", "document"],
"trigger": { "type": "schedule|event|manual", "phrase": "every morning" },
"archetype": "prior_authorization", // catalog match when one fits
"archetype_matches": [{ "key": "...", "name": "...", "score": 5.0 }],
"confidence": 0.85,
"clarifying_question": null, // set when action = "unclear"
"suggestion": { "endpoint": "POST /v1/workers/from-archetype/prior_authorization", "body": { … } }
}, "executed": null }
How it understands: a deterministic linguistic pass always runs — normalized tokens, synonym expansion ("bot/assistant/helper" → agent, "squad/crew/group" → team, "keep an eye on/watch/track" → monitor), light stemming, one-typo tolerance ("agnet", "shedules" still land), entity and trigger extraction, and semantic scoring against the 30-archetype catalog. A structured model pass (request_interpretation) then refines the classification — enrichment, never load-bearing, and its output is validated against known actions, so a hallucinated action or archetype can't slip through. Vague input ("do stuff") returns action: "unclear" with one clarifying question instead of guessing.
With "execute": true (owner/admin/editor; viewers get 403) the request is also carried out — the worker is hired (catalog match) or compiled from the raw sentence (custom), the team is built, the research program created, the workflow drafted, the task created and routed to the best-matching worker, or the question answered from org memory — and executed describes what was created. Every execution is audited (interpret.executed).
The same understanding layer powers POST /v1/teams/build composition fallback, and the builder chat (POST /v1/builder/sessions/{id}/messages) continues to run its own conversation router for multi-turn building.
29. Production hardening v3 — budgets, repair, deletion (frontend-relevant)
Everything in this section is live. The changes below are the ones the frontend must know about.
29.1 AI budget caps → handle HTTP 402 everywhere
Every org has daily AI spend caps (model calls). When the cap is reached, any endpoint that would spend money — hiring a worker, building via chat, generating a workflow, interpreter execute — returns:
402 { "detail": "Daily AI budget reached ($15.03 of $15.00). Hiring a new worker paused until tomorrow — raise DAILY_AI_BUDGET_USD to change this.", "code": "ai_budget_exceeded" }
Frontend handling: treat 402 + code ai_budget_exceeded as a calm, non-error state. Resolve the loading state and show an inline card: title "Daily AI budget reached", the detail string as body, no red styling — this is a safety feature working, not a failure. In the Home chat, render it as an assistant message. Never leave a spinner running on 402.
Autonomous research has its own sub-cap. When it's exhausted, programs go quiet until the next UTC day and the audit log gets a research.budget_paused entry. On the research program page, when the most recent audit entry for the program is research.budget_paused, show a muted status line: "Paused for today — daily research budget reached. Resumes tomorrow." No user action needed or offered.
29.2 Worker generation: fast path, slow path, and honest failure
Hiring the same archetype in the same connector context is now served from a compile cache: expect responses in ~1 second. First-time hires and custom instructions still run the full pipeline (10–40s). So the hire flow must handle BOTH instantly-resolved and long-running requests with the same code path.
Model calls are hard-capped at 120 seconds server-side, so no request hangs longer than ~2 minutes. REQUIRED frontend behavior for hire/build/generate: on any failure or timeout, always resolve the loading state and show an inline error card with a Retry button. Never leave a spinner running after a request has settled.
29.3 Self-repairing builds: SPEC_REPAIRED events and workflow "repairs"
The compiler now auto-corrects invented tool names before validation, so builds that previously failed with "uses unknown tool X" succeed.
- Build event streams may include a new event type
SPEC_REPAIREDwith atextlike "tool repaired: 'search_pubmed' -> search_research". Render it in the build timeline as a small neutral info row (wrench icon), not a warning or error. POST /v1/workflows/generatenow returns{ graph, repairs: [".."], validation: {valid, errors} }. Ifrepairsis non-empty, show one muted line above the canvas: "{n} tool reference(s) auto-corrected" with the notes in a tooltip or expandable. Load the returned graph as before.
29.4 Delete account
POST /v1/auth/delete-account body { "password": "...", "confirm": "DELETE" }
- 200 →
{ deleted: true, workspaces_purged: n, workspaces_left: n } - 400 → confirm wasn't the literal word DELETE
- 403 → wrong password
- 409 → user is the only owner of a workspace that has other members; detail explains ownership must be transferred first
Frontend: Settings > General gets a "Danger zone" section at the bottom — bordered card, the ONE place red is allowed. Button "Delete account" opens a modal that explains plainly: workspaces where you are the only member are permanently erased (workers, runs, everything); workspaces with other members keep their data and remove your membership; this cannot be undone. The modal requires the password field and typing DELETE into a confirm field; the destructive button stays disabled until both are filled. On 200: clear the session and route to the sign-in page with a plain goodbye message. On 409: show the detail text with a link to Settings > Members to transfer ownership.
29.5 Connector config responses never contain secrets
GET /v1/connectors strips any config key containing secret/key/token/password/credential. The frontend must never expect credentials back after saving them — render saved connectors from status + non-secret config only, and show credential fields as write-only (blank inputs with placeholder "saved — enter to replace").
29.6 Research workers work out of the box
The two research tools search 20 public scientific databases (PubMed, ClinicalTrials.gov, ChEMBL, PubChem, UniProt, openFDA, Open Targets, PDB, and more) with no connector setup — they're keyless public APIs. Catalog connector recommendations for research sources remain as guidance about preferred sources, not prerequisites: never present a research worker as blocked on connections.
29.7 New/changed reference summary
| Endpoint / surface | Change |
|---|---|
| POST /v1/auth/delete-account | NEW — see 29.4 |
| POST /v1/workers/from-archetype/{key} | may return in ~1s (cache) or 402 (budget); response unchanged otherwise |
| POST /v1/workflows/generate | response adds repairs: [] |
| Build event stream | new event type SPEC_REPAIRED |
| GET /v1/connectors | config guaranteed secret-free |
| Audit log | new action research.budget_paused |
| Any model-spending endpoint | can return 402 ai_budget_exceeded |
