Cereal docs
Build a research agent

Control publication with a hook

An optional extension that applies your application's policy before a tool runs.

The research task needs no approval. This optional lesson shows how a hook changes behavior at one point in the loop, without moving the loop into your application.

Choose the policy in your application

The starter defines this tool-call hook:

src/agent.ts
      onToolCall: async (call) => {
        if (call.name !== "publish_brief") return { decision: "allow" }
        return { decision: "ask", ask: { prompt: "Publish this research brief?" } }
      }

Source reads are allowed. Publishing asks a person. A production hook can consult your own workspace settings and return allow, deny, or ask; see decision hooks.

In Ada's conversation, first research the Orion launch, then ask “Publish that brief.” Cereal records a pending decision before dispatching publish_brief. The starter's tool returns a publication preview; it does not publish or store content.

Answer through an authorized route

The browser reads agent.approvals.pending(sessionId) through its authenticated server route, then sends the decision to /api/sessions/:id/decide:

src/app.ts
          if (
            action === "decide" &&
            typeof body.toolCallId === "string" &&
            (body.decision === "allow" || body.decision === "deny")
          ) {
            try {
              return json(
                await client.sessions.decide({
                  id,
                  toolCallId: body.toolCallId,
                  decision: body.decision,
                  actor: member.id
                })
              )
            } catch (error) {
              if (
                error instanceof DecisionNotPendingError ||
                error instanceof DecisionExpiredError
              ) {
                return json(
                  {
                    error: "This approval was answered or expired. Refresh the pending approvals."
                  },
                  409
                )
              }
              throw error
            }
          }

Ownership and request-origin checks run first. The actor is the authenticated member, not a browser-supplied name. A duplicate or expired decision returns a recoverable 409; the UI refreshes pending approvals.

Check both outcomes

Allow the call and look for preview: true with the brief and its source IDs in the tool result. In a new session, request publication and deny it; that tool call must not execute.

The session parks at idle with stopReason: requires_action, then continues after the decision. A stream token can display the pending request but cannot authorize it. For a real publishing service, enforce workspace permissions in the handler and use a durable idempotency key for external writes. Hook deliveries can be retried.

Next: recover and debug.

On this page