> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Supervise another agent

> Have an org admin grant one agent read access to another agent's logs, runs, data and versions, read them from code, and gate the supervised agent's writes on who called

After this guide, one [agent](/concepts/agents) (the supervisor) reads another agent's logs, workflow runs, chosen data collections, versions, source and environment variable names from its own code, and the supervised agent (the target) accepts changes from the supervisor only through tools that check who is calling. Use it for an agent that watches or maintains other agents, such as an operations agent that reviews the failures of a support agent every morning.

*The supervision members are live on the platform; their lua-cli types arrive in the next CLI release.*

**Before you begin**

* Two agents in the **same** organization. A grant across organizations is refused, and so is an agent supervising itself.
* An org `admin` or `owner` to create the grant ([Organizations and roles](/concepts/organizations-and-roles)). Nothing in an agent's code, its environment or a push can create one.
* An API key or session for that admin ([REST API overview](/reference/rest/overview)).

<Steps>
  <Step title="Choose the actions">
    A grant gives the supervisor one or more actions on each target. Grant the least that does the job.

    | Action               | The supervisor may                                                                                                                                                                                           |
    | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | `observe`            | Read logs without message bodies, workflow runs as metadata, the Data collections the grant names, versions and persona versions, source files and environment variable names                                |
    | `observe-content`    | Also read log message bodies. `observe` alone never returns them                                                                                                                                             |
    | `act`                | Be recognised by the target as its supervisor: the target's tools see `Lua.request.invokedBy.supervisor === true` when the supervisor invokes it                                                             |
    | `preview`, `release` | Chat an unpromoted version, promote a version and activate a persona. Not generally available: the platform refuses both unless they have been switched on for your environment, and they are off by default |

    `act` and `release` are write actions and are never transitive: an agent that is itself supervised cannot hold one, an agent that holds one cannot be supervised, and two agents cannot supervise each other.
  </Step>

  <Step title="Grant supervision (org admin)">
    An org admin posts the grant to the organization. Find the organization id as `orgId` in `lua agents --json --ci`.

    ```bash theme={null}
    curl -sS -X POST "https://api.heylua.ai/admin/orgs/<<YOUR_ORG_ID>>/supervision-grants" \
      -H "Authorization: Bearer <<YOUR_API_KEY>>" \
      -H "Content-Type: application/json" \
      -d '{
        "supervisorAgentId": "<supervisor-agent-id>",
        "targetAgentIds": ["<target-agent-id>"],
        "actions": ["observe", "act"],
        "dataCollections": ["tickets", "escalations"]
      }'
    ```

    | Field                | Type                 | Meaning                                                                                                                                       |
    | -------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
    | `supervisorAgentId`  | `string`, required   | The agent that supervises                                                                                                                     |
    | `targetAgentIds`     | `string[]`, required | 1 to 50 explicit agent ids of the organization. `*` is refused                                                                                |
    | `actions`            | `string[]`, required | Any of `observe`, `observe-content`, `preview`, `release`, `act`                                                                              |
    | `dataCollections`    | `string[]`           | The target's [Data](/reference/sdk/data) collections the supervisor may read. Needs `observe`. Without it, the supervisor reads no collection |
    | `releaseWorkflowIds` | `string[]`           | The supervisor's own workflows whose steps may release on the target. Needs `release`                                                         |

    `dataCollections` and `releaseWorkflowIds` take explicit names only: at most 50 each, matching `[A-Za-z0-9_.:-]{1,128}`, never `*`. The grant, not the target's code, decides which collections are readable.

    The route needs the `members:assign-role` permission on the organization, which `admin` and `owner` hold. It answers `201` with one grant per target:

    ```json Output theme={null}
    [
      {
        "id": "4b7c1e0a-…",
        "supervisorAgentId": "<supervisor-agent-id>",
        "targetAgentId": "<target-agent-id>",
        "actions": ["observe", "act"],
        "dataCollections": ["tickets", "escalations"],
        "createdByUserId": "<your-user-id>",
        "createdAt": "2026-09-28T09:12:44.120Z"
      }
    ]
    ```

    Posting again for the same supervisor and target replaces that grant's actions and lists. Every change is recorded in the organization's audit log.

    | Status                           | When                                                                                                                                                                                                      |
    | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `400`                            | A required field is missing, a target is `*` or not a live agent of the organization, the supervisor is among the targets, or `dataCollections` / `releaseWorkflowIds` come without `observe` / `release` |
    | `409` `supervisor_is_supervised` | The supervisor is itself supervised and asks for `act` or `release`                                                                                                                                       |
    | `409` `target_holds_release`     | A target holds `act` or `release` on another agent, so it cannot be supervised                                                                                                                            |
    | `409` `mutual_supervision`       | A target already supervises the supervisor                                                                                                                                                                |
  </Step>

  <Step title="List and revoke grants">
    `GET` lists the organization's grants and needs `members:manage`; `DELETE` revokes one by its `id`, answers `204`, and also works when the target has been archived.

    ```bash theme={null}
    curl -sS "https://api.heylua.ai/admin/orgs/<<YOUR_ORG_ID>>/supervision-grants" \
      -H "Authorization: Bearer <<YOUR_API_KEY>>"

    curl -sS -X DELETE "https://api.heylua.ai/admin/orgs/<<YOUR_ORG_ID>>/supervision-grants/<grant-id>" \
      -H "Authorization: Bearer <<YOUR_API_KEY>>"
    ```
  </Step>

  <Step title="Read the target from the supervisor">
    In the supervisor's code, the [`Agents`](/reference/sdk/agents#supervision) members read what the grant allows. A job that summarises the target's errors every morning:

    ```ts src/jobs/ReviewSupportAgentJob.ts theme={null}
    import { LuaJob, Agents, env } from 'lua-cli';

    export default new LuaJob({
      name: 'review-support-agent',
      description: 'Summarise the support agent’s errors and failed runs from the last day',
      schedule: { type: 'cron', expression: '0 7 * * *', timezone: 'Europe/London' },
      timeout: 120,
      async execute() {
        const target = env('SUPPORT_AGENT_ID');
        if (!target) throw new Error('SUPPORT_AGENT_ID is not set');

        const errors = await Agents.logs(target, { level: 'error', limit: 100 });
        const failedRuns = await Agents.runs(target).list({ status: 'failed', limit: 20 });
        const openEscalations = await Agents.data(target).query('escalations', { status: { $eq: 'open' } });

        return {
          errors: errors.length,
          failingTools: [...new Set(errors.map((row) => row.toolName).filter(Boolean))],
          failedRuns: failedRuns.map((run) => ({ runId: run.runId, workflow: run.workflowName, reason: run.reason })),
          openEscalations,
        };
      },
    });
    ```

    Log rows carry `timestamp`, `level`, `logSource`, `primitiveName` and `toolName`, never a user id, and a `message` only with `observe-content`. Runs carry metadata only, never inputs or outputs. A collection the grant does not name fails with `supervision_data_not_granted`.
  </Step>

  <Step title="Accept changes from a supervisor">
    When another agent invokes the target, the target's code sees [`Lua.request.invokedBy`](/reference/sdk/lua#requestinvokedby), set by the platform from the verified call. The platform supplies the identity; the target keeps its own policy. A tool that changes something should allow a write from an agent only when all three hold:

    * `invokedBy.supervisor` is `true`, which needs an admin's `act` grant on this agent;
    * `invokedBy.agentId` is on the target's own list of supervisors it trusts;
    * every field the call changes is on this tool's list of fields a supervisor may change.

    A turn without `invokedBy` came from a person, so apply your usual owner check. `delegatedUserId`, and the `userId` of a turn an agent created, never grant owner rights.

    ```ts src/skills/tools/UpdateEscalationPolicyTool.ts theme={null}
    import { LuaTool, Lua, Data, env } from 'lua-cli';
    import { z } from 'zod';

    const SUPERVISOR_FIELDS = new Set(['threshold', 'quietHours']);

    function listFromEnv(key: string): string[] {
      return (env(key) ?? '').split(',').map((s) => s.trim()).filter(Boolean);
    }

    function mayWrite(fields: string[]): { ok: true } | { ok: false; reason: string } {
      const caller = Lua.request.invokedBy;
      if (caller) {
        if (!caller.supervisor) return { ok: false, reason: 'the calling agent is not a supervisor of this agent' };
        if (!listFromEnv('SUPERVISOR_AGENT_IDS').includes(caller.agentId)) {
          return { ok: false, reason: 'the calling agent is not a trusted supervisor' };
        }
        const denied = fields.filter((f) => !SUPERVISOR_FIELDS.has(f));
        if (denied.length) return { ok: false, reason: `a supervisor may not change ${denied.join(', ')}` };
        return { ok: true };
      }
      const owners = listFromEnv('OWNER_USER_IDS');
      return Lua.request.userId && owners.includes(Lua.request.userId)
        ? { ok: true }
        : { ok: false, reason: 'only the owner may change the escalation policy' };
    }

    export default class UpdateEscalationPolicyTool implements LuaTool {
      name = 'update_escalation_policy';
      description = 'Change the escalation threshold, quiet hours or on-call contact';
      inputSchema = z.object({
        threshold: z.number().int().min(1).optional(),
        quietHours: z.string().optional(),
        onCallEmail: z.string().email().optional(),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        const fields = Object.keys(input).filter((k) => input[k as keyof typeof input] !== undefined);
        const decision = mayWrite(fields);
        if (!decision.ok) return { updated: false, reason: decision.reason };
        const entry = await Data.create('escalation_policy', { ...input, changedAt: Date.now() });
        return { updated: true, id: entry.id };
      }
    }
    ```

    Here a supervisor may tune `threshold` and `quietHours`, but only the owner may change who is paged. When another agent invokes the target, the target always runs with its own persona, model and tools: the caller cannot replace them ([What reaches another agent](/reference/sdk/agents#what-reaches-another-agent)).
  </Step>

  <Step title="Verify">
    Run the supervisor's job once, then read the target's logs as its owner. Every supervision call the target allowed, and every call refused for lack of a grant, is a `supervisor_access` line in the **target's** logs, naming the supervisor and the member it called.

    ```bash theme={null}
    lua jobs trigger --job-name review-support-agent
    lua logs --agent-id <target-agent-id> --type all --limit 20
    ```

    The lines have the source `supervision`. From the next lua-cli release, `--type supervision` shows only them; lua-cli 3.39.4 does not accept that value yet.

    If the line cannot be written, the call fails instead of running unrecorded (`supervision_audit_unavailable`).
  </Step>
</Steps>

## Options you may need

### Test the supervisor locally

In `lua test`, the read members use your own developer credential and return what you can see, with the same projections: logs without user ids or message bodies, runs as metadata, environment variable names only. `data`, `source.get`, `list` and `get` are not available offline (`supervision_unavailable_offline`), because the readable collections are part of the grant.

### Limits

| Limit                                                                          | Value                                                                |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| Supervision calls per supervising agent                                        | 60 a minute                                                          |
| `logs` window and rows                                                         | 24 hours by default, 7 days at most; 50 rows by default, 100 at most |
| `runs.list` rows                                                               | 20 by default, 50 at most                                            |
| `data.query` rows                                                              | 20 by default, 100 at most                                           |
| Source file size                                                               | 512 KB                                                               |
| Targets per grant request; names per `dataCollections` or `releaseWorkflowIds` | 50; 50                                                               |

## If it isn't working

<AccordionGroup>
  <Accordion title="Agents.list() returns []">
    **Cause** The supervisor holds no grant on any agent of its organization. **Fix** Have an org admin post a grant, then list grants to check `supervisorAgentId` is the agent your code runs as.
  </Accordion>

  <Accordion title="supervision_not_granted">
    **Cause** The grant lacks the action the member needs: every read needs `observe`. **Fix** Post the grant again with the missing action; the new list replaces the old one.
  </Accordion>

  <Accordion title="supervision_not_found">
    **Cause** The target is in another organization or does not exist; the two are reported the same way on purpose. **Fix** Check the id with `lua agents --json --ci`.
  </Accordion>

  <Accordion title="supervision_data_not_granted">
    **Cause** The collection is not in the grant's `dataCollections`. **Fix** Post the grant again with the collection named.
  </Accordion>

  <Accordion title="invokedBy.supervisor is false although a grant exists">
    **Cause** Only `act` sets it; `observe`, `preview` and `release` never do. **Fix** Add `act` to the grant if the target should accept that agent's writes.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Agents reference" href="/reference/sdk/agents#supervision">Every supervision member, its return shape and its errors.</Card>
  <Card title="Lua reference" href="/reference/sdk/lua#requestinvokedby">`Lua.request.invokedBy` on an invoked turn.</Card>
  <Card title="Compose agents" href="/build/compose-agents">Hand part of a task to another agent with `Agents.invoke`.</Card>
  <Card title="Organizations and roles" href="/concepts/organizations-and-roles">Who is an org admin.</Card>
</Columns>
