API
Agents, skills, harnesses, swarm, wake, inbox, and runs for the foundry service.
Discovery and ops
| Path | Method | Notes |
|---|---|---|
/ | GET | Service discovery (enveloped) |
/api-docs | GET | Swagger UI |
/openapi.yaml, /openapi.json | GET | Spec from binary |
/healthz | GET | Liveness |
/readyz | GET | Readiness (Postgres ping) |
Versioned API
| Path | Method | Purpose |
|---|---|---|
/v1/agents | GET, POST | List / create. Default list is top-level agents that are not archived, including expired ephemeral agents. lifetime=persistent keeps only the persistent ones. lifetime=ephemeral lists swarm members that are not archived, newest first, a page at a time. status=archive lists archived top-level agents. desired_state narrows by run state, q matches the name, slug, address, instructions, owner, or creator, and project_id takes comma-separated ids matched alongside q, because foundry stores the project id and cannot read workspace for its name. Create takes slug or a slug proposed from name. lifetime defaults to persistent, and ephemeral is refused with 422 unless the call is agent-originated. The list holds only agents the caller owns or is a member of; a platform admin sees every agent unless the request sends X-Flow-As-Member: true |
/v1/agents/by-slug/{slug} | GET | Read one agent by unique slug. Communications uses this for inbound mail |
/v1/agents/{id} | GET, PUT, DELETE | Read / edit / delete a persistent agent. Delete stops the harness and removes swarm children |
/v1/agents/{id}/start | POST | Persistent: desired_state running + Deployment. Ephemeral: desired_state running + a Job under a bumped generation. An expired or archived agent is refused |
/v1/agents/{id}/swarm | POST | Spawn ephemeral members |
/v1/agents/{id}/children | GET | Agents this one spawned, archived ones included, newest first. This is what the Ephemeral tab reads |
/v1/agents/{id}/members | GET | Who may use the agent: the owner first, then editors and viewers in the order they were added. A swarm member or coding agent has none of its own (409 child_agent) |
/v1/agents/{id}/members/{principal} | PUT, DELETE | PUT gives someone viewer or editor, or changes their role. DELETE takes it away. Both need editor access, except removing yourself. The owner cannot be changed here (409 agent_owner). Both return everyone with access afterwards |
/v1/agents/{id}/coding-agents | POST | Start an ephemeral coding agent for one repository. The parent must be persistent and active. harness_id picks the harness and must declare repo_scope; omit it for Coding Harness V1. X-Flow-Agent-Id is accepted only from the service identity and must be that parent |
/v1/agents/{id}/jira/read | POST | Read the one Jira issue named in the parent instructions. Summary, status, and description. Any other key is refused |
/v1/agents/{id}/jira/investigate | POST | Read that issue and return a comment from read-only identity, linkage, lease, Grafana, Carson, and Salto checks. The tool logs in to Carson, reads the delegation user_id, and checks Salto permitted doors when that id is present. Nothing is written. No email means the comment says so and DeepSky is not called. A missing DeepSky or Grafana credential is 503 and the harness keeps running. A missing Carson login is not, when those three credentials are set |
/v1/agents/{id}/move-ins | POST | list_move_ins. One Yardi property's incoming residents from DeepSky's legacy PMS connector, with Yardi codes and whether each email and phone is present, valid, and shared. No name, email, or phone number. Cached 30 minutes. 503 without the connector credential; 502 when the connector read fails |
/v1/agents/{id}/resident-access | POST | check_resident_access. One incoming resident's read-only DeepSky, Carson, and Salto facts, looked up by Yardi codes. No name, email, or phone number. Cached an hour per person. 404 when the person is not in the property's move-ins |
/v1/agents/{id}/unit-mapping-gaps | POST | list_unit_mapping_gaps. The Yardi units DeepSky's legacy PMS connector could not map in the last day, from its lease sync logs in Loki, with the DeepSky spaces that could match each one. Codes only. Cached 30 minutes. 503 without the DeepSky or Grafana credential; 502 when a read fails |
/v1/agents/{id}/unit-mapping | POST | apply_unit_mapping. Uploads one unit mapping to the connector and resyncs the unit's tenants, only when a fresh DeepSky read finds exactly one space with the unit's name. 409 when it does not, or when the connector has not logged the unit as unmapped in the last day. A repeat within a day returns the first result without writing. 429 after 10 in a day |
/v1/agents/{id}/missing-yardi-emails | POST | list_missing_yardi_emails. The Yardi tenants DeepSky's legacy PMS connector skipped in the last day because Yardi has no email for them, from its lease sync logs in Loki. Codes only. Cached 30 minutes. 503 without the DeepSky or Grafana credential; 502 when the read fails |
/v1/agents/{id}/yardi-emails-from-carson | POST | fill_yardi_emails_from_carson. Calls the connector's POST /legacy-pms-connector/residents/{tenantCode}/email/from-carson, which fills each missing email on the lease from the same-named Carson resident, and resyncs the lease when any was written. Returns each person's outcome; no email passes through. 409 when the tenant was not logged in the last day or the connector's writeback is off for the property. A repeat within a day returns the first result without calling the connector. 429 after 30 in a day |
/v1/agents/{id}/jira/recommendations | POST | Post one recommendation on that issue and store the attempt. A second call returns the stored row |
/v1/agents/{id}/jira/claims | POST | Start one ephemeral coding agent for deepsky or platform. deepsky sets the pull request branch to development and the path prefix to deepsky. Refused while another coding run is still open |
/v1/agents/{id}/jira/reports | POST | Comment the pull request URL on this parent's open coding attempt once the run has finished. A finished run with no pull request says so |
/v1/swarms/{id}/dismiss | POST | Spin down every member of that swarm |
/v1/agents/{id}/stop | POST | Persistent stop, or rest one ephemeral agent: desired_state becomes completed and status stays active, so it can start again |
/v1/agents/{id}/expire | POST | Harness marks an ephemeral agent expired once its run finishes or its wall clock runs out, then exits. A run that only used up its step allowance leaves the agent active |
/v1/agents/{id}/archive | POST | Set status to archive and stop the harness |
/v1/agents/{id}/restart | POST | Persistent always-on: bump generation and recycle the Deployment. Expired ephemeral: set status back to active, bump generation, move ttl_at four hours ahead, and start a new Job from the instructions on the row. Start still refuses an expired agent. Archive is refused |
/v1/agents/{id}/run | POST | Manual on-demand wake |
/v1/agents/{id}/call-secret | POST | Issue a one-time secret for the call path. Plaintext is returned once |
/v1/agents/on-demand/{slug} | POST | Call a persistent on-demand agent. Cloudflare JWT plus X-Flow-On-Demand-Secret |
/v1/agents/{id}/wake | POST | Enqueue wake (comms, dream, harvest, manual) |
/v1/agents/{id}/inbox | GET | Harness drain |
/v1/agents/{id}/runs | GET, POST | Wake history; harness records a completed wake |
/v1/agents/{id}/logs | GET, POST | Harness start, heartbeat, and wake lines. POST is the process checking in |
/v1/agents/{id}/memories | GET, POST, DELETE | What the agent has kept. GET lists newest first; q searches. POST from the harness (source: "agent", archive) or from a person (core). DELETE on the collection purges every row; only a person with editor access can purge |
/v1/agents/{id}/memories/{memoryId} | PATCH, DELETE | PATCH pins or demotes (tier). DELETE drops one row. Only a person with editor access; shared service tokens cannot edit or delete memories |
/v1/agents/{id}/memories/consolidate | POST | Harness applies a consolidation pass it already ran |
/v1/agents/{id}/memories/touch | POST | Harness marks recalled rows as used |
/v1/agents/{id}/transcript-reviews | GET, POST | Agent-owned page interpretations. POST is idempotent; GET uses a meeting-date window of at most 31 days and bracket pagination capped at five rows. The harness revalidates original sources |
/v1/agents/{id}/transcript-progress | GET | Contiguous reviewed sequence for an exact record and source version; gaps prevent a complete-reading claim |
/v1/agents/{id}/research-checkpoint | GET, POST | Authoritative opt-in research continuation. POST requires Idempotency-Key and the current version; stale writes and backward movement within a scan return 409. Transcript mode supports source-page continuation and deferred scan restarts; see transcript research |
/v1/agents/{id}/thoughts | GET | The assembled thoughts block the next cycle will carry. Same renderer the harness uses |
/v1/skills/upload-url | POST | Mint a documents write SAS. Not a command. PUT the zip/tar, then create |
/v1/skills | GET, POST | List / create a skill with its first version (markdown, document_id, or blob_name). markdown is stored as SKILL.md. q matches the name, description, category, or owner, including the owner's name |
/v1/skills/{id} | GET, PUT | Read / edit name, description, category |
/v1/skills/{id}/body | GET | The version's SKILL.md. Optional version_id; omit it for the latest. A missing file returns an empty markdown and a note |
/v1/skills/{id}/versions | POST | Another archive on the same skill |
/v1/harnesses | GET | Harness registry. Each row includes allows_skills, permitted tools, and features |
/v1/tools | GET | Tool catalog (name, family, description, schema). Optional harness_id narrows to what that harness permits |
/v1/agents/{id}/workspace-conversation | POST | Harness records the workspace conversation it opened |
/v1/audit | GET | Foundry audit log, newest first. Filter by domain, event type, actor, entity, all exact. q is free text, matching the summary, the actor, or the event type. Rows about agents the caller cannot read are left out of the rows and the total; skill rows are not |
/v1/audit/{id} | GET | One audit row. 404 when it is about an agent the caller cannot read |
PUT /v1/agents/{id} also takes slug, model (auto or a broker wire_id),
memory_model (auto means the agent's own model), harness_id, max_turns,
skills: [{ skill_id, version_id? }], and tools: ["search", …].
max_turns is the model steps one Agent Harness V1 evaluation may take. The minimum
is 10. Omit it on create to use 10, and on update to keep the stored value. A higher
value is kept.
A null skill pin means latest. An empty tools list grants nothing extra.
Always-granted tools (remember, recall on V1) come with the harness and are
dropped from stored grants. A tool not on the pinned harness, or a skill on a
harness with allows_skills=false, is a 422. A save that changes instructions,
model, memory model, skills, harness, or tools bumps generation. The agent
record also includes workspace_conversation_id when the harness has opened a
transcript. contributes_to_knowledge defaults to false. The harness copies that
value onto workspace conversations it opens. A save that omits the field leaves
the stored value. Changing run_type or harness_id on a running agent is a 409;
stop it first.
Raw PUT /v1/agents/{id} is not a partial-update contract: callers must supply
name, instructions, project_id, practice, and conversation_id to preserve
their values. The Foundry tools
read the latest definition and merge the supplied fields before making that PUT.
GET /v1/harnesses includes always_granted. GET /v1/tools?harness_id=
marks those tools with always_granted: true. The store and thoughts renderer
live in services/foundry/internal/harness/memory; V1 (cmd/harnessV1) decides
when to call them.
Every agent record includes created_by, slug, email, and last_heartbeat_at
(set when the harness posts a log line). created_by is the creator's FID, or
agent:<id> when an agent created it. email is ${slug}@ai.flow.life in prod and
${slug}@dev-ai.flow.life in every other environment. The slug is unique. A create
without slug proposes one from name. call_secret_pending is true when a one-time
secret is waiting. The hash is never returned. A log row is { kind, message, created_at }
with kind of started, heartbeat, wake, error, skill, info, or warn.
A skill line names the tool (ran send_sms) and does not include the result.
info and warn are the work cycle: config, granted tools, skips, and parse
checks. The same lines are printed to harness stdout.
send_email, send_sms, send_announcement, git_commit, and open_pull_request take an
optional sources argument: a list of {kind, id}, where kind is evidence (a workspace
record_id) or insight (an intelligence insight id). It names the records or insights the
message or change was based on. It is optional because an agent can also act on the web, its
prompt, or its own reasoning, and no call is refused for leaving it out. After a successful
call, the harness posts the sources to POST /v1/agents/{id}/action-sources. It adds any
UUID written into the message body, subject, commit message, or pull request title and body,
since agents already put insight ids there and the send path removes them before delivery.
Each row also stores read_in_run: whether a read tool such as search or get_insight
returned that id earlier in the same run. A Harness V1 run is one evaluation, and a coding
run is one task. GET /v1/observer/sources?ids= lists the actions that cited any of up to
200 ids, newest first, for the agents the caller may list. The Network Observer uses it to
draw a record's path. GET /v1/agents/{id}/sources lists what one agent cited, together with
every agent below it in its parent chain, such as its ephemeral and coding agents. Each row's
agent_id names the agent that cited it. The caller needs at least the viewer role on the
agent. The Sources tab on the Agents page and the Sources section of an ephemeral agent read
this route, and link each source to its insight page or to the record in Evidence Review.
Each row also has "Show path" for the record's path on the Network Observer. An insight row
reads the insight's citations and shows one link for each workspace record it quotes.
The Conversation tab shows the workspace conversation the harness writes for that agent, and links to the same thread on the Conversations page. An ephemeral agent shows its own conversation on the Ephemeral tab.
POST /v1/agents/on-demand/{slug} is the public call path for a persistent on-demand
agent. It does not start a harness pod. The caller needs a Cloudflare Access JWT
(Cf-Access-Jwt-Assertion) and the one-time secret in X-Flow-On-Demand-Secret.
The secret is consumed on a successful call. Start sets desired_state=running so
the path accepts calls; Stop turns it off.
The audit log is foundry's own (same pattern as ADR 0043): foundry-api's publisher writes one sentence per agent or skill outbox event. auditListener writes the same row from the bus. The command that changed the row does not write the audit row.
Enveloped All /v1/* JSON uses the Response envelope: The template service wraps every JSON response in {header, payload}. A success payload is {data} (a record or a list) plus optional meta.pagination; accounting still returns bare bodies and RFC 9457 errors until the divergence is resolved..
Every agent has one owner, owner_fid, the person it acts as. Other people get editor or
viewer access through the members routes. A viewer reads. An editor does everything except
delete and archive, sharing and tools included. Only the owner deletes or archives. A swarm
member or coding agent follows the agent at the top of its parent chain. Someone with no access
gets 404, the same as for an agent that does not exist; a member without the role gets 403 with
code insufficient_role. Every agent record carries caller_role. A platform admin reads every
agent and changes none, unless the request sends X-Flow-As-Member: true. See
docs/decisions/0078-foundry-agents-have-members.md.
Cedar: The authorization engine; each service embeds a Cedar policy and evaluates every mutating request against the actor's principal record. still gates the action itself: staff manage, start, and run. A person can also
call with a broker credential they minted for themselves. The JWT's email is the caller, the same
as their Access session. communications and the scheduler use a service token. The shared service
token is also what the harness uses: it reads the inbox, posts runs and logs, and calls the agent
endpoints behind any Foundry tools the agent is
granted. With X-Flow-Agent-Id, a service-token call acts as that agent's owner; the Foundry tools
always send it. Without it, the service token keeps the access it had before agents had members.
Communications also reads GET /v1/agents/by-slug/{slug} to attach inbound mail. The
scheduler POSTs /wake only. The same scheduler token may also POST
/internal/jobs/daily-briefs and /internal/jobs/weekly-coaching. See
Daily briefs.
Daily briefs
| Path | Method | Purpose |
|---|---|---|
/internal/jobs/daily-briefs | POST | Write today's brief for each recipient and store the text. Does not send email. email limits it to one person, date is YYYY-MM-DD, and regenerate=true writes a new revision and marks the old one superseded. HTTP 503 when any recipient is not complete |
/v1/briefs | GET | Read your own brief. date is YYYY-MM-DD. Defaults to today in America/New_York. recipient or all=true reads other people's published briefs and needs foundry::ReadAnyBrief, which platform admins hold. [email protected] may read those published briefs. Drafts need the same action and drafts=true. Does not write a revision |
/v1/briefs | POST | Store pasted text as one person's brief. Needs foundry::WriteBrief, which platform admins hold. The morning job leaves that text in place unless regenerate=true |
/v1/briefs/feedback | POST | Record wrong_scope, immaterial, already_resolved, missing, ungrounded, or useful for one brief. Does not change the text |
/v1/briefs/speech | POST | MP3 for one passage of your own stored brief. The passage has to be in that brief. The response is audio/mpeg, not the usual envelope |
Weekly coaching
For the agent-desktop "You" page (ADR 0111). Every route but the job is the caller's own and takes no person, so nobody, including a manager or a platform admin, can read another person's coaching. The agent never reads it.
| Path | Method | Purpose |
|---|---|---|
/internal/jobs/weekly-coaching | POST | Write last week's coaching for each person in COACH_RECIPIENTS who has not turned it off. email limits it to one person, week is any YYYY-MM-DD in the week, and regenerate=true replaces a stored week. HTTP 503 when anyone could not be written |
/v1/me/coaching | GET | Whether coaching is on, and the caller's latest report |
/v1/me/coaching/setting | PUT | { "enabled": false } turns it off and deletes every stored report and its feedback. true turns it back on |
/v1/me/coaching/feedback | POST | Useful or not useful on one point of the caller's own report. Another person's report is a 404 |
A report holds two parts. Meeting time is counted in code. A one-line summary gives the coached
week's recorded meetings, their length and how many the person spoke in. Zoom records carry no end,
so a meeting's length runs to its last line. Then, from the past four weeks of meetings the
person was in, calls in a personal room left out: for a recurring meeting, how often it met, for how long, how often they spoke, and how
many decision claims intelligence drew from it. A series is raised when it took at least two hours and
they spoke in at most a quarter of the meetings read, or when it met at least three times with no
decisions. How you communicate is one model call over the person's own lines from the week's meetings
and threads, for the people in COACH_COMMUNICATION_RECIPIENTS. It looks for a decision without an
owner or date, an ask without an owner or deadline, a repeated question, and a meeting they ran that
ended without next steps. Code keeps at most three points, and only those whose quote is at the line
named, is the person's own words (or the end of a meeting they ran), and names nobody else.
A third part covers what other people said about the person that week (ADR 0116): lines naming them
from records they may open, as on the "You" page, read with the lines around each. One model call looks
for someone waiting on them, credit, a concern, and a misunderstanding, at most one point of each. The
quote keeps its speaker's name; the suggestion names nobody. The report carries these as about_you,
and the job's answer as about_you and about_you_points.
Headers
| Header | Required | Purpose |
|---|---|---|
Idempotency-Key | Every mutating command except /wake (key is derived) | Stable across retries. A key replays only on the method and path that first used it; anywhere else it is a 409 |
Content-Type | POST, PUT | application/json |
X-Flow-Agent-Id | Optional, with the service token only | The calling agent's id. The call acts as that agent's owner. On POST /v1/agents the new agent gets that owner and a copy of the calling agent's members |
X-Flow-As-Member | Optional | true keeps a platform admin to the agents they own or are a member of |
X-Dev-Principal | local DEV_AUTH=1 only | The caller email |
Events carry ids, lifetime, run type, and reason: agent.started, agent.stopped,
agent.woken, swarm.dismissed, agent.skills_changed, agent.tools_changed,
skill.created, skill.versioned. agent.member_added, agent.member_role_changed, and
agent.member_removed carry the agent id, the principal, and the role.
No bodies.