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).

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

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.

const conversationId = 'conv_123';
const { data: pending } = await r.integrations.listPendingExecutions(conversationId);
for (const exec of pending) {
console.log(`${exec.tool_name}, ${exec.params}`);
if (exec.tool_name === 'GMAIL_SEND_EMAIL') {
await r.integrations.approveExecution(exec.id); // runs the tool
} else {
await r.integrations.rejectExecution(exec.id); // blocks it
}
}

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.

const session = await r.auth.signIn({ email, password });
const key = await r.auth.createApiKey({
name: 'read-only-reporting',
scopes: ['posts:read', 'agents:read'],
projectId: 'proj_123', // app-member key, scoped to one project
}, session.token);
console.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.

const key = await r.auth.createApiKey({
name: 'ops-team-key',
scopes: ['projects:write', 'agents:write'],
organizationId: 'org_123', // team-scoped, org-bound
}, 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.

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

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.

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

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.

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

Track outcomes against a task

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

await r.dispatcher.recordOutcome('task_123', {
metric_name: 'p95_latency_ms',
before_value: '820',
after_value: '310',
notes: 'Added the missing index on orders.created_at.',
});
const { data: outcomes } = await r.dispatcher.taskOutcomes('task_123');
console.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.

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

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.