The hook plane is how logic runs around an operation without editing the service that owns it. The owner of an operation declares a hook point before its commit. Other biomes contribute handlers. A binding connects a handler to a hook point. When the operation runs, the handlers see the candidate change and can let it through, change it, hold it for a decision, or refuse it.

Hooks are pre-commit only. Anything that should happen after a commit is an event subscription, not a hook.

For the old manifest lifecycle block, see Lifecycle hooks. The hook plane is the way to run logic around an operation.

Modes, verdicts and outcomes

A hook point supports one or more modes:

ModeWhat a handler may do
admitAllow or deny the candidate. It cannot change it.
transformPatch the candidate, only in the fields the hook point lists as writableFields.
deferHold the operation until a decision is made. The result is a decision requirement.

Each handler returns one verdict: allow, deny, patch or defer. The answers are folded into one outcome for the operation:

OutcomeMeaning
proceedThe operation continues, with any patches applied.
denyThe operation is refused.
deferThe operation waits on a decision requirement.
failA hook could not be evaluated. The operation does not commit.

A patch outside writableFields is not applied.

Declaring a hook point

A service declares hook points on its component in xema-biome.json, under components[].hookPoints[]. Each entry has these fields:

FieldPurpose
idThe hook point's identifier.
operationThe operation it guards.
modes[]The modes offered, each with the action kinds a handler may use.
writableFieldsThe fields a transform handler may patch. Empty means none.
inputProjectionThe parts of the candidate a handler is shown.
participation[]Whether bindings may be required, optional, or both.
limitsFan-out, payload size, concurrency and the deadline.

The agent runtime declares one like this on its tool-execution operation:

json
1{
2  "id": "agent-session.tool.execute",
3  "operation": "agent-session.tool.execute",
4  "modes": [{ "mode": "admit", "actionKinds": ["capability", "function"] }],
5  "writableFields": [],
6  "inputProjection": ["/tool", "/args"],
7  "participation": ["required", "optional"],
8  "limits": {
9    "maxFanOut": 16,
10    "maxPayloadBytes": 262144,
11    "maxConcurrency": 8,
12    "deadlineMs": 3000
13  }
14}

The workflow runtime declares hook points the same way. The limits you write are a request: the platform caps them.

Evaluating a hook point

The operation evaluates its hook point before it commits, using HookComposer from the service SDK (@xemahq/hook-composition, wired in by @xemahq/xema-service-nest):

ts
1import { HookOutcomeKind } from '@xemahq/contracts/hook';
2
3const result = await composer.evaluate({ hookPointId, context, candidate });
4if (result.kind === HookOutcomeKind.Proceed) {
5  // Authorize result.document as the final operation, then commit it.
6}

The composer calls the bound handlers in parallel under one deadline and the platform limits, then folds their answers. It fails closed: any failure it knows of ends as the fail outcome, and the operation does not commit.

Contributing a handler

A handler is an ordinary capability. Publish it the way you publish any capability, then bind it. Nothing in the manifest says "this is a hook handler".

Binding a handler

A binding is a runtime resource, not a manifest field. Create one with POST /hook-bindings on the capability registry.

  • Who may bind. An org admin binds for the org Space. A user binds for their own Space.
  • Body. hookPointId, space, mode, participation, handler (the action kind and the descriptor at its expected hash), projection and executeAs.
  • Inheritance. A binding applies to the Space it is made in and to the Spaces below it. See Spaces.
  • Required and optional. A Space may suppress an inherited optional binding. A required binding is removed only with DELETE /hook-bindings/:id/required.
  • Refusals. A binding that cannot be made returns 422 with the code HOOK_BINDING_REFUSED. The response lists every reason, not only the first.
  • Delegation. POST /hook-bindings/:id/authorize asks again for the execute delegation the handler runs under.

Agent sessions: readiness and denials

For agent sessions, two read endpoints show how hooks affected a session:

  • GET /sessions/{id}/hook-readiness reports whether the hooks bound to the session are ready.
  • GET /sessions/{id}/tool-denials lists the tool calls that were denied.