Observability & audit
Every tool call is a queryable record: execution logs, per-session activity, and signed platform webhooks — the material for debug panels, alerting, and compliance answers.
Scenario: A customer asks Acme: "what exactly did the assistant do on our account last Tuesday?" Compliance asks: "who approved that refund?" On-call asks: "why are Gmail sends failing since 14:00?" All three answers come from the same three surfaces.
The execution log
Every execution — agent tool-call, MCP session call, background actions.execute, approval replay — writes one record:
// Slice by tool, agent, status, or time window:
const { items, nextCursor } = await protobox.executions.list({
status: 'error',
startDate: '2026-07-22T00:00:00Z',
endDate: '2026-07-23T00:00:00Z',
limit: 100,
});
// Drill into one:
const record = await protobox.executions.get(executionId);
// → tool, status, arguments, result, timing — the "what exactly ran" answerThis is the join point for the other recipes: approvals carry an executionId after replay, webhook events carry one on completion/failure, and session events reference the same records.
Per-session activity
Conversation-scoped questions go to the session itself:
// The 90-day feed, newest first, cursor-paginated:
const page = await protobox.sessions.listEvents(sessionId, { type: 'execute', status: 'error' });
// The at-a-glance answer for a debug panel:
const session = await protobox.sessions.get(sessionId);
// session.summary → { toolCalls, errors, lastCall, toolkitsUsed, toolkitsInScope }listEvents filters by type (search / execute / connection / sandbox) and status — "show me every failed execute in this conversation" is one call. One nuance: calls to code-bundle tools are typed sandbox, not execute — an execute-only filter silently misses them, so query both types when the session mixes integration and code tools.
Platform webhooks
One endpoint per workspace receives signed events — push-based monitoring instead of polling:
const hook = await protobox.webhooks.register({ url: 'https://api.acme.com/protobox/events' });
// hook.secret is returned ONLY on create/rotate — store it for verification.Verify every delivery
verifyWebhook is a pure function (no HTTP): it checks the X-Protobox-Signature header (HMAC-SHA256 over "{t}.{rawBody}", 5-minute default tolerance) and returns the typed event:
import { verifyWebhook, WebhookVerificationError } from '@protoboxai/sdk';
app.post('/protobox/events', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = verifyWebhook(req.body.toString('utf8'), req.get('X-Protobox-Signature') ?? '', {
secret: process.env.PROTOBOX_WEBHOOK_SECRET!,
});
} catch (err) {
if (err instanceof WebhookVerificationError) return res.status(400).end();
throw err;
}
res.status(200).end(); // ack fast, process async
void handleEvent(event);
});Verify against the raw request body — a re-serialized JSON body will fail the signature. Rotate the secret with webhooks.register({ url, rotateSecret: true }).
The event vocabulary
| Event | Fires when | Useful for |
|---|---|---|
connection.activated | A user finished connecting an app | Flip UI state, kick off first-sync |
connection.revoked | A credential was revoked | Reflect disconnect, pause dependent jobs |
execution.pending_approval | A policy paused a call | Notify your approval inbox |
execution.completed | An execution finished | Progress tracking for async work |
execution.failed | An execution failed | Alerting |
Event payloads are deliberately minimal — ids, integration, status, your callerId. Never arguments, results, or credential material; fetch details via executions.get when you need them.
An audit answer, end-to-end
"Who approved the refund on ticket T-4821, and did it run?"
const { items } = await protobox.approvals.list({ status: 'approved' });
const approval = items.find((a) => a.arguments['ticketId'] === 'T-4821');
// approval.decidedBy, approval.decidedAt → who and when
const execution = approval?.executionId
? await protobox.executions.get(approval.executionId)
: undefined; // → did it run, and what happenedProduction notes
- Correlate with
requestId. Every SDK error carries the platformrequestId(fromx-request-id) — log it, and support conversations become lookups. - Ack webhooks in under a second and process on a queue; slow receivers get treated like failing ones.
- Alert on
execution.failedrates, not single failures — anot_connectedburst usually means one user's token expired, while a cross-userupstream_errorburst means a provider incident.
Related
- Human-in-the-loop — the approval half of the audit trail
- Error handling reference
Human-in-the-loop
Gate risky tools behind human sign-off: a toolset policy pauses the call, your product shows the approval, and approve() replays it server-side.
White-label
Keep Protobox invisible: consent screens that carry your brand, tool names that speak your product's vocabulary, and connect flows that live in your UI.