Flow
ServicesFoundry Service

API

Agents, skills, harnesses, swarm, wake, inbox, and runs for the foundry service.

Discovery and ops

PathMethodNotes
/GETService discovery (enveloped)
/api-docsGETSwagger UI
/openapi.yaml, /openapi.jsonGETSpec from binary
/healthzGETLiveness
/readyzGETReadiness (Postgres ping)

Versioned API

PathMethodPurpose
/v1/agentsGET, POSTList / 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}GETRead one agent by unique slug. Communications uses this for inbound mail
/v1/agents/{id}GET, PUT, DELETERead / edit / delete a persistent agent. Delete stops the harness and removes swarm children
/v1/agents/{id}/startPOSTPersistent: desired_state running + Deployment. Ephemeral: desired_state running + a Job under a bumped generation. An expired or archived agent is refused
/v1/agents/{id}/swarmPOSTSpawn ephemeral members
/v1/agents/{id}/childrenGETAgents this one spawned, archived ones included, newest first. This is what the Ephemeral tab reads
/v1/agents/{id}/membersGETWho 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, DELETEPUT 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-agentsPOSTStart 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/readPOSTRead the one Jira issue named in the parent instructions. Summary, status, and description. Any other key is refused
/v1/agents/{id}/jira/investigatePOSTRead 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-insPOSTlist_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-accessPOSTcheck_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-gapsPOSTlist_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-mappingPOSTapply_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-emailsPOSTlist_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-carsonPOSTfill_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/recommendationsPOSTPost one recommendation on that issue and store the attempt. A second call returns the stored row
/v1/agents/{id}/jira/claimsPOSTStart 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/reportsPOSTComment 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}/dismissPOSTSpin down every member of that swarm
/v1/agents/{id}/stopPOSTPersistent stop, or rest one ephemeral agent: desired_state becomes completed and status stays active, so it can start again
/v1/agents/{id}/expirePOSTHarness 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}/archivePOSTSet status to archive and stop the harness
/v1/agents/{id}/restartPOSTPersistent 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}/runPOSTManual on-demand wake
/v1/agents/{id}/call-secretPOSTIssue a one-time secret for the call path. Plaintext is returned once
/v1/agents/on-demand/{slug}POSTCall a persistent on-demand agent. Cloudflare JWT plus X-Flow-On-Demand-Secret
/v1/agents/{id}/wakePOSTEnqueue wake (comms, dream, harvest, manual)
/v1/agents/{id}/inboxGETHarness drain
/v1/agents/{id}/runsGET, POSTWake history; harness records a completed wake
/v1/agents/{id}/logsGET, POSTHarness start, heartbeat, and wake lines. POST is the process checking in
/v1/agents/{id}/memoriesGET, POST, DELETEWhat 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, DELETEPATCH 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/consolidatePOSTHarness applies a consolidation pass it already ran
/v1/agents/{id}/memories/touchPOSTHarness marks recalled rows as used
/v1/agents/{id}/transcript-reviewsGET, POSTAgent-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-progressGETContiguous reviewed sequence for an exact record and source version; gaps prevent a complete-reading claim
/v1/agents/{id}/research-checkpointGET, POSTAuthoritative 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}/thoughtsGETThe assembled thoughts block the next cycle will carry. Same renderer the harness uses
/v1/skills/upload-urlPOSTMint a documents write SAS. Not a command. PUT the zip/tar, then create
/v1/skillsGET, POSTList / 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, PUTRead / edit name, description, category
/v1/skills/{id}/bodyGETThe 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}/versionsPOSTAnother archive on the same skill
/v1/harnessesGETHarness registry. Each row includes allows_skills, permitted tools, and features
/v1/toolsGETTool catalog (name, family, description, schema). Optional harness_id narrows to what that harness permits
/v1/agents/{id}/workspace-conversationPOSTHarness records the workspace conversation it opened
/v1/auditGETFoundry 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}GETOne 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

PathMethodPurpose
/internal/jobs/daily-briefsPOSTWrite 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/briefsGETRead 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/briefsPOSTStore 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/feedbackPOSTRecord wrong_scope, immaterial, already_resolved, missing, ungrounded, or useful for one brief. Does not change the text
/v1/briefs/speechPOSTMP3 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.

PathMethodPurpose
/internal/jobs/weekly-coachingPOSTWrite 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/coachingGETWhether coaching is on, and the caller's latest report
/v1/me/coaching/settingPUT{ "enabled": false } turns it off and deletes every stored report and its feedback. true turns it back on
/v1/me/coaching/feedbackPOSTUseful 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

HeaderRequiredPurpose
Idempotency-KeyEvery 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-TypePOST, PUTapplication/json
X-Flow-Agent-IdOptional, with the service token onlyThe 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-MemberOptionaltrue keeps a platform admin to the agents they own or are a member of
X-Dev-Principallocal DEV_AUTH=1 onlyThe 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.

On this page