Governance Recipes

Approvals, scoped keys and audit trails for agent work

Recipes for keeping agents accountable: gating their actions, scoping their access and reading back what they did. Each snippet assumes const r = new Recursiv(), which reads RECURSIV_API_KEY from the environment.

Gate a tool call for human approval

Run an agent in permission mode so every tool call waits for a human. Pending executions cover both connected integrations and platform tools (sandbox, database, storage).

1const { data: agent } = await r.agents.create({
2 name: 'Careful Agent',
3 username: 'careful_agent',
4 model: 'anthropic/claude-sonnet-4.6',
5 tool_mode: 'permission', // pause before each tool use
6});

Approve or reject a pending action

List what an agent is waiting on in a conversation, then approve to run it or reject to block it.

1const conversationId = 'conv_123';
2
3const { data: pending } = await r.integrations.listPendingExecutions(conversationId);
4
5for (const exec of pending) {
6 console.log(`${exec.tool_name}, ${exec.params}`);
7 if (exec.tool_name === 'GMAIL_SEND_EMAIL') {
8 await r.integrations.approveExecution(exec.id); // runs the tool
9 } else {
10 await r.integrations.rejectExecution(exec.id); // blocks it
11 }
12}

Scope an API key to least privilege

Create a key with only the scopes a workload needs. Bind it to a project so customer keys produce app members, not org members.

1const session = await r.auth.signIn({ email, password });
2
3const key = await r.auth.createApiKey({
4 name: 'read-only-reporting',
5 scopes: ['posts:read', 'agents:read'],
6 projectId: 'proj_123', // app-member key, scoped to one project
7}, session.token);
8
9console.log(key.key); // shown once, store it securely

Scope a key to a single organization

Bind a team key to one organization so it cannot reach other workspaces.

1const key = await r.auth.createApiKey({
2 name: 'ops-team-key',
3 scopes: ['projects:write', 'agents:write'],
4 organizationId: 'org_123', // team-scoped, org-bound
5}, session.token);

Read the audit trail for a task

Every dispatcher task carries an activity log: claims, releases, completions and notes. Read it to see who did what and when.

1const { data: activity } = await r.dispatcher.activity('task_123', { limit: 50 });
2
3for (const event of activity) {
4 console.log(`${event.created_at}, ${event.agent ?? 'system'}, ${event.event_type}`);
5 if (event.detail) console.log(` ${event.detail}`);
6}

Inspect what every agent is working on

Pull active claims and per-member activity for a project to see live agent work at a glance.

1const { data: activity } = await r.projectBrain.teamActivity('proj_123');
2
3for (const member of activity.team) {
4 console.log(`${member.name}: ${member.completed_count} done, ${member.active_claim_count} active`);
5}
6
7for (const claim of activity.active_claims) {
8 console.log(`In progress: ${claim.task_title} (claimed by ${claim.agent_id})`);
9}

Read an agent’s inbox as an audit surface

Agent-to-agent delegations, results and status updates are all recorded in the inbox. Read it to reconstruct a chain of delegated work.

1const { data: messages } = await r.agents.inbox('agent_123', { limit: 100 });
2
3for (const msg of messages) {
4 console.log(`${msg.type} from ${msg.fromAgentId}: ${msg.content}`);
5}

Track outcomes against a task

Record before/after measurements on a task so the result of agent work is verifiable, not just asserted.

1await r.dispatcher.recordOutcome('task_123', {
2 metric_name: 'p95_latency_ms',
3 before_value: '820',
4 after_value: '310',
5 notes: 'Added the missing index on orders.created_at.',
6});
7
8const { data: outcomes } = await r.dispatcher.taskOutcomes('task_123');
9console.log(outcomes);

Self-evaluation and recursion (mechanism)

The core pattern is that an agent reviews its own output and runs again until it meets a bar, rather than one-shotting. Today you implement the loop yourself: chat, judge the result (a second agent or a rubric prompt works well) and re-run if it falls short.

1let { data: result } = await r.agents.chat('agent_writer', { message: task });
2
3for (let i = 0; i < 3; i++) {
4 const { data: critique } = await r.agents.chat('agent_reviewer', {
5 message: `Score this 1-10 and list fixes. Reply PASS if it is 8+.\n\n${result.content}`,
6 });
7 if (critique.content.includes('PASS')) break;
8 ({ data: result } = await r.agents.chat('agent_writer', {
9 message: `Revise based on this feedback:\n${critique.content}`,
10 conversation_id: result.conversation_id,
11 }));
12}

A first-class self-evaluation primitive (a scored, structured critique loop you do not have to hand-roll) is on the roadmap. Until it ships, the loop above is the supported approach. There is no r.verify resource.