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:
| Mode | What a handler may do |
|---|---|
admit | Allow or deny the candidate. It cannot change it. |
transform | Patch the candidate, only in the fields the hook point lists as writableFields. |
defer | Hold 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:
| Outcome | Meaning |
|---|---|
proceed | The operation continues, with any patches applied. |
deny | The operation is refused. |
defer | The operation waits on a decision requirement. |
fail | A 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:
| Field | Purpose |
|---|---|
id | The hook point's identifier. |
operation | The operation it guards. |
modes[] | The modes offered, each with the action kinds a handler may use. |
writableFields | The fields a transform handler may patch. Empty means none. |
inputProjection | The parts of the candidate a handler is shown. |
participation[] | Whether bindings may be required, optional, or both. |
limits | Fan-out, payload size, concurrency and the deadline. |
The agent runtime declares one like this on its tool-execution operation:
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):
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),projectionandexecuteAs. - 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
422with the codeHOOK_BINDING_REFUSED. The response lists every reason, not only the first. - Delegation.
POST /hook-bindings/:id/authorizeasks 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-readinessreports whether the hooks bound to the session are ready.GET /sessions/{id}/tool-denialslists the tool calls that were denied.
Related
- Lifecycle hooks: the older manifest
lifecycleblock. - Decisions: how a deferred operation gets its answer.
- Capabilities: what a handler is published as.
- Manifest: where hook points are declared.
- Process topology: the same operation code runs in any deployment shape.