Documentation
On this page

Example: a small agentic dev team

Two people and two coding agents share issues, tasks, and pull requests. Agents claim work, spend their own budgets, and submit code; CI receives signed webhooks and reports back; reviews follow the team's rules; a maintainer's assistant approves over MCP; and the team's git host is connected, so an agent reads only the repositories it is allowed. No server feature was added for it.

This page walks through one complete scenario: the model, the actions, the CI integration, and every request in order, including the ones that are refused. Everything here uses the public API, so you can load the same model into your own organization and replay it. The model needed nothing beyond the features the other examples use.

Tasks in the web app, with who claimed each one and its status

Who takes part

IdentityConnects withTeams
Dana, a maintainera person, in your app and through an assistantREST, with a session token; MCP, with an OAuth grant of ontology:read actions:apply, and a personal MCP token of connectors:use connectors:writedevelopers, maintainers
Eli, a developera person, in your appREST, with a session tokendevelopers
ada and bixcoding agents, as agent accountsREST, with API tokens; ada also MCP, with an MCP token of connectors:usedevelopers, agents
ci-botthe CI system’s agent accountREST, with an API tokenci

No team holds a write permission. Everything below changes only through actions, so an agent’s API token can claim a task or submit a pull request, but cannot edit a task’s claimant or a pull request’s CI result directly (403).

The model

Object typeHolds
reponame (unique), default_branch, and the review policy agents_review_agents
issuenumber (a sequence), title, body, reporter, status
tasktitle, claimant, claimed_at, status: open, claimed, in_review, or done
pull_requestnumber (a sequence), title, author, head_sha, status, ci_status and ci_sha, approved_by and approved_sha, merged_by, merged_at
reviewreviewer, reviewer_kind, sha, verdict: approve or request_changes; body
budgetagent, limit, spent, in credits
runagent, estimate, cost, status, started_at, finished_at

Commit shas are strings with a regex rule. Links join a task to its issue and repository, a pull request to its task and repository, a review to its pull request, and a run to its task and budget.

The seed creates the api repository, with agents_review_agents off, and two budgets: 50 credits for ada and 20 for bix.

Budgets are private

Every type is visible to every member except budget, which is restricted with agent as its access property:

JSON
PUT /v1/ontology/object-types/budget/access
{ "restricted": true, "access_properties": ["agent"],
  "grants": [
    { "team_id": "<agents>", "permission": "view_objects", "scope": "assigned" },
    { "team_id": "<maintainers>", "permission": "view_objects" } ] }

Each agent sees its own budget and maintainers see all of them. Another agent’s budget does not exist for an agent, so it cannot even name one in an action.

The actions

ActionGranted toDoes
open_issuedevelopersfiles a numbered issue
plan_taskmaintainersadds a task for an open issue in a repository
claim_taskdeveloperstakes an open task; a task has one claimant
start_runagentsreserves credits from the caller’s own budget for work on the caller’s task
finish_runagentsrecords what the run cost, and charges the budget that instead of the estimate
submit_prdevelopersopens a numbered pull request for the task the caller claimed; tells CI
push_commitsdevelopersmoves the caller’s pull request to a new head commit, resetting its approval and CI result; tells CI
report_cicirecords the CI result for the pull request’s head commit only
review_prdevelopersapproves the head commit or requests changes
merge_prmaintainersmerges a pull request approved and passed at its head, and completes the task
set_review_policymaintainersallows or forbids agents approving agents’ pull requests in a repository

One claimant per task

claim_task is one criterion and one update:

JSON
{ "criteria": [
    { "name": "unclaimed", "message": "the task is claimed already",
      "condition": { "op": "eq", "left": { "object": "task", "property": "status" },
                     "right": { "value": "open" } } } ],
  "edits": [
    { "op": "update_object", "object": { "param": "task" },
      "properties": { "claimant": { "caller": "id" }, "claimed_at": { "now": true },
                      "status": { "value": "claimed" } } } ] }

The criterion alone would not be enough if two agents read the open task at the same moment. But an action locks its objects and checks their versions again before it writes, so of two concurrent claims one commits and the other gets 409 version_conflict, or, if it reads the task after the first committed, 422 at criteria.unclaimed.

Budgets

start_run takes the task, a budget, and an estimate. Its criteria check that the task is the caller’s, that the budget is the caller’s, and, in Python, that the estimate fits:

Python
def check(ctx):
    b = ctx['objects']['budget']['properties']
    left = b['limit'] - b['spent']
    if ctx['params']['estimate'] > left:
        return 'the run needs ' + str(ctx['params']['estimate']) + ' credits; ' + str(left) + ' are left'
    return True

The edits create the run, link it, and add the estimate to spent. finish_run replaces the estimate with the cost, after a linked criterion checks that the run was charged to that budget.

Review rules

review_pr has six criteria:

CriterionHolds when
openthe pull request is open or approved
of_repoit belongs to the repository passed (linked)
currentthe review is of the pull request’s head_sha
not_authorthe caller is not its author
agent_reviewsthe caller is a human, or the author is, or the repository’s agents_review_agents is on
maintainer_approves_agentsan approval by a person of an agent’s pull request comes from a maintainer
JSON
{ "name": "agent_reviews",
  "message": "agents review agents' pull requests only where the repository allows it",
  "condition": { "op": "or", "conditions": [
    { "op": "eq", "left": { "caller": "kind" }, "right": { "value": "human" } },
    { "op": "eq", "left": { "user": { "object": "pull_request", "property": "author" }, "field": "kind" },
      "right": { "value": "human" } },
    { "op": "eq", "left": { "object": "repo", "property": "agents_review_agents" }, "right": { "value": true } } ] } }

{"user": ..., "field": "kind"} looks up the kind of the user in the pull request’s author, so nothing about the author is copied onto the pull request. The last criterion checks a team:

JSON
{ "name": "maintainer_approves_agents",
  "message": "only maintainers approve an agent's pull request for people",
  "condition": { "op": "or", "conditions": [
    { "op": "ne", "left": { "param": "verdict" }, "right": { "value": "approve" } },
    { "op": "eq", "left": { "caller": "kind" }, "right": { "value": "agent" } },
    { "op": "eq", "left": { "user": { "object": "pull_request", "property": "author" }, "field": "kind" },
      "right": { "value": "human" } },
    { "op": "contains", "list": { "caller": "teams" }, "value": { "team": "maintainers" } } ] } }

Any developer may request changes, but when a person approves an agent’s work, that person is on maintainers. Agents reviewing agents are governed by agent_reviews instead.

merge_pr checks that the pull request is approved at its head and that CI passed the head, so a push after an approval needs a new approval and a new CI result.

CI as a webhook receiver

submit_pr, push_commits, and merge_pr each have a webhook effect to CI. CI is an ordinary HTTPS server:

  1. At setup, it reads each action type’s webhook secret as an owner: GET /v1/ontology/action-types/submit_pr/webhook-secret, and the same for push_commits and merge_pr.
  2. For each delivery, it picks the secret by the payload’s action and checks x-happy-enterprise-signature: sha256= and the hex HMAC-SHA256 of "<x-happy-enterprise-timestamp>.<body>". Anything else is refused.
  3. It builds the commit in params.sha, and reports back as ci-bot:
HTTP
POST /v1/actions/report_ci/apply
Authorization: Bearer heat_...
{ "params": { "pull_request": "<pr>", "sha": "a1b2c3d", "passed": true },
  "idempotency_key": "<x-happy-enterprise-delivery>" }

The delivery id is the same on every redelivery of a webhook, so using it as the idempotency key turns a redelivered webhook into a replay, not a second report. report_ci only accepts the pull request’s current head, so a slow build of an older commit cannot mark a newer one as passed. See Effects for the webhook format.

Setting it up

As an owner, through the REST API:

  1. Create the four teams, with no permissions, and the three agent accounts (members API, teams API). Add Dana, Eli, ada, and bix to developers, Dana to maintainers, ada and bix to agents, and ci-bot to ci.
  2. Create the object types and link types (schema API), and restrict budget as above.
  3. Create the action types and grant each to its teams (actions API), and give CI the three webhook secrets.
  4. Create the repository and the two budgets.
  5. Connect the team’s git host (see 8. The git host).

1. Dana plans the work

Dana opens issue 1, “Rate-limit the login endpoint”, and plans two tasks for it in api: “Add a limiter” and “Document the limits”. Eli trying plan_task gets 403: it is granted to maintainers only. ada trying to PATCH itself in as the limiter’s claimant also gets 403.

2. Two agents, one task

ada and bix claim the limiter at the same moment:

HTTP
POST /v1/actions/claim_task/apply
{ "params": { "task": "<limiter>" } }

One gets 201. The other gets 409 version_conflict, or 422 at criteria.unclaimed if it arrived after the first committed. Say ada won. bix claiming the limiter again gets criteria.unclaimed, and takes the docs task instead.

3. Runs and budgets

Refused by a criterion. bix asks for a 25-credit run on the docs task, from a 20-credit budget:

JSON
422 { "error": { "code": "criteria_failed",
      "details": [{ "path": "criteria.within_budget",
                    "message": "the run needs 25 credits; 20 are left" }] } }

Refused as unknown. bix tries to charge ada’s budget instead: 422 at params.budget. To bix, ada’s budget does not exist; asking for it by id is 404.

bix starts a 15-credit run. ada starts an 18-credit run on the limiter and finishes it at a cost of 12, so ada’s budget shows 12 spent. Dana, a maintainer, sees both budgets.

4. A pull request, and CI

ada submits the limiter as commit a1b2c3d, with an idempotency key. The answer is lost and ada sends it again: 200, replayed: true. There is one pull request, number 1, with ada as its author and ci_status: "pending", and the task is in_review.

After commit, CI receives one submit_pr delivery, not two: the replay applied nothing, so it queued no effect. CI verifies the signature and reports a1b2c3d as passed with the delivery id as its key. A redelivery’s report is a replay.

5. Reviews

Refused by the policy. bix approves ada’s pull request: criteria.agent_reviews. Agents do not approve agents in api.

Refused twice. ada approves ada’s own pull request. Both reasons come back: criteria.not_author and criteria.agent_reviews.

Refused by a team check. Eli approves ada’s pull request: criteria.maintainer_approves_agents. Eli is a developer, not a maintainer.

Eli requests changes instead. ada pushes commit e4f5a6b: the pull request is open again, and its CI result pending.

Refused as stale. CI’s late report for a1b2c3d arrives now: 422 at criteria.head, “the pull request moved on to another commit”. The push_commits delivery comes in, and CI reports e4f5a6b as passed.

6. Dana’s assistant approves

Dana’s MCP client lists the open pull requests (query_objects with status = open) and finds one. It approves the commit it saw first:

JSON
{ "tool": "action_review_pr",
  "arguments": { "pull_request": "<pr>", "repo": "<api>", "sha": "a1b2c3d",
                 "verdict": "approve", "body": "Approved by Dana's assistant" } }

→ error { "code": "criteria_failed",
          "details": [{ "path": "criteria.current", "message": "review the pull request's head commit" }] }

With e4f5a6b, the head, it succeeds. The approval is Dana’s: the pull request’s approved_by is Dana, and the audit log records the application as kind: human, via: mcp.

Eli tries to merge: 403. Dana merges: the pull request is merged, the task done, and CI’s merge_pr webhook is sent.

7. Agents review agents

Dana turns agents_review_agents on for api (set_review_policy). bix finishes its run at a cost of 9, and submits the docs as pull request 2, commit 0c0ffee. ada approves it, which the policy now allows.

Dana tries to merge before CI has reported: 422 at criteria.ci_passed. CI reports, and Dana merges.

8. The git host

The team’s git host is an MCP server with three tools: list_repos and read_file {repo, path}, which read, and comment {repo, issue, text}, which writes. It holds the repositories api, docs, and infra, and infra holds production secrets. The owner connects it with the credential the git host gave the team:

JSON
POST /v1/connectors
{ "name": "git", "display_name": "Tiny git host", "url": "https://git.tinyteam.example/mcp",
  "auth": { "kind": "bearer", "token": "..." } }

POST /v1/connectors/git/sync lists the three tools as pending. The owner reads each definition and enables it by its hash, marking comment as a write:

JSON
PATCH /v1/connectors/git/tools/comment
{ "state": "enabled", "hash": "<the reviewed hash>", "access": "write" }

Then the grants:

Toolagentsmaintainers
list_reposyesyes
read_fileonly {"repo": {"allow": ["api", "docs"]}}any repository
commentnoyes

The owner issues ada an MCP token of connectors:use, for 30 days:

HTTP
POST /v1/mcp-tokens
{ "name": "ada", "scopes": ["connectors:use"], "user": "<ada>", "expires_in_days": 30 }

201 { "id": "<token>", "token": "hemt_...", "scopes": ["connectors:use"], ... }

ada reads api. With that token, ada’s tools/list shows exactly git.list_repos and git.read_file, and read_file’s repo is an enum of api and docs. Reading api’s README.md returns the file, after a line marking it as external content.

Refused: infra. ada asks for infra’s README.md. The grant allows only api and docs, so the call is a tool error and never reaches the git host.

Refused: commenting. git.comment is not granted to agents, and ada’s token lacks connectors:write besides, so the call is a tool error.

Dana comments. Dana issues a personal token of connectors:use connectors:write, and comments “on it” on api issue 1. The git host receives the comment from the team’s credential: every call goes out as the same remote identity.

The call log (GET /v1/connectors/git/calls) shows all four calls, oldest first:

ToolByStatus
read_file (api)adaok
read_file (infra)adadenied
commentadadenied
commentDanaok

What everyone saw

The audit log tells people, agents, and the assistant apart. Its three reviews, newest first:

Reviewkindvia
ada approves pull request 2agentrest
Dana’s assistant approves pull request 1humanmcp
Eli requests changes on pull request 1humanrest

Every refused request, and both replays, left no record and changed nothing.

What this shows

  • Agents are members with rules, not admins with tokens. The coding agents hold no write permission. They claim, spend, and submit only through actions that check the task is theirs and the budget is theirs.
  • Races have one winner. Two agents claiming one task cannot both get it; the loser is told why.
  • Review policy is data. Who may approve whom is in the criteria, in the teams people are on, and in a repository flag a maintainer sets, and it applies to every caller alike.
  • Integrations get the same guarantees. CI verifies where a webhook came from, reports through an action, and its retries and late results are harmless.
  • An assistant acts as its person. Dana’s assistant can approve because Dana can, and the log says it was Dana, through MCP.
  • Outside tools are granted, not handed over. Agents never hold the git host’s credential. They call only the reviewed tools granted to their team, with the arguments their grant allows, and every call, refused or not, is in the log.

What it does not do yet

  • One identity at the git host. The git host sees every call as the team’s credential, not as ada or Dana; only the call log tells them apart.
  • Budgets do not reset in this example. A schedule per budget could apply a reset action each period; none is set up. Run costs are what the agents report, not metered.
  • The git host’s commits are not the pull requests’ commits. The shas on pull requests are the ones agents and CI report.
Esc
↑ ↓ navigate↵ open