Result handlers
A result handler receives either durable artifact exports or oversized tool-result archives from a run. Signed, idempotent, one artifact per POST.
Use cases: copy generated media or explicit workspace exports into your storage, archive oversized tool outputs, and provide a fallback archive when workspace spill fails.
Methods
| Method | Route |
|---|---|
| novo.resultHandlers.register({...}) | POST /v1/result-handlers |
| novo.resultHandlers.list() | GET /v1/result-handlers |
| novo.resultHandlers.get(id) | GET /v1/result-handlers/:id |
| novo.resultHandlers.update(id, {...}) | PATCH /v1/result-handlers/:id |
| novo.resultHandlers.delete(id) | DELETE /v1/result-handlers/:id |
| novo.resultHandlers.rotateSecret(id, { secret }) | POST /v1/result-handlers/:id/rotate-secret |
Type signature
type ResultHandlersNamespace = {
register(input: {
kind: 'artifact';
name: string;
url: string;
secret: string;
requestTimeoutMs?: number | null;
metadata?: Metadata;
}): Promise<ResultHandler>;
list(): Promise<Page<ResultHandler>>;
get(resultHandlerId: string): Promise<ResultHandler>;
update(resultHandlerId: string, input: {
name?: string;
url?: string;
requestTimeoutMs?: number | null;
metadata?: MetadataPatch;
}): Promise<ResultHandler>;
delete(resultHandlerId: string): Promise<{ id: string; object: 'result_handler'; deleted: true }>;
rotateSecret(resultHandlerId: string, input: { secret: string }): Promise<ResultHandler>;
};
Register
const handler = await novo.resultHandlers.register({
kind: 'artifact',
name: 'archive',
url: 'https://api.acme.dev/novo/results',
secret: 'rh_secret_…',
});
Attach to an agent via agents.update:
await novo.agents.update(agent.id, {
artifacts: {
exports: { resultHandlerId: handler.id },
},
});
artifacts.exports receives generated media deliverables and explicit workspace_export_artifact({ path }) outputs by default. Ordinary document create/edit/fill/export outputs, document previews, extracted images, render artifacts, and oversized tool-output overflow do not use this export path. Browser screenshots are not exported unless artifacts.exports.include.browserScreenshots is true; browser downloads are not exported unless artifacts.exports.include.browserDownloads is true.
artifacts.oversizedToolResults is only for oversized tool-output recovery/archive. When the agent has a spill-capable workspace, oversized tool results first land in the workspace so the agent can inspect them; the result handler receives an archival copy and is used as a fallback if workspace spill fails. Without workspace spill, the handler is the primary recovery store.
Agent artifact refs default to artifacts.stream.urls: 'durable-only': Novo emits your handler's durable URL when one is returned, otherwise it emits an id-only transfer handle. Set artifacts.stream.urls: 'include-novo-signed' only when your consumer explicitly wants temporary Novo signed URLs in public/model-visible tool results.
The delivery shape
For every handler dispatch, Novo POSTs to your URL:
POST {url} HTTP/1.1
Content-Type: application/json
Novo-Signature: t=…,v1=…
Novo-Timestamp: …
Novo-Request-Id: req_…
Novo-Org-Id: org_…
Novo-Agent-Id: agent_…
Novo-Run-Id: run_…
Novo-Result-Handler-Id: rh_…
Novo-Result-Kind: artifact
Oversized tool-result artifacts use this body:
{
"runId": "run_…",
"threadId": "thread_…",
"agentId": "agent_…",
"toolName": "mcp__docs__search",
"toolCallId": "tc_…",
"stepNumber": 2,
"mediaType": "application/json",
"bytes": 7340032,
"sha256": "…",
"content": "{\"large\":true}"
}
Return a JSON object describing your durable artifact, the object you saved on your side:
{
"artifactId": "tra_123",
"url": "https://storage.acme.dev/novo/tra_123",
"ref": {
"kind": "drive",
"id": "drive_ref_42",
"block": "[[block:drive:drive_ref_42]]",
"url": "https://storage.acme.dev/novo/tra_123"
},
"metadata": { "bucket": "agent-archives" }
}
Every artifact producer shares one response contract: return at least one durable locator, artifactId or url. Returned URLs and references must be durable; Novo never calls the handler again merely to refresh a replayed result. The complete JSON response is limited to 256 KiB; metadata is optional and limited to 64 KiB, and paste-ready reference fields are also bounded. A response with neither durable locator is rejected as result_handler_response_invalid; ref/block alone is not enough. (The oversized tool-result recovery path is stricter: it needs an artifactId to persist.)
For artifact exports, ref describes the same customer-side object in a form the agent can cite or paste: optional kind, id, url, and a ready-to-paste block. Novo projects it onto model-visible artifact.ref and, when present, artifact.block on generated media, explicit workspace exports, browser screenshots, and browser downloads. Handlers that still emit a top-level block scalar or transitional suggested_block_refs: { drive: "..." } are accepted and normalized into the same projection. When you configure no export handler for one of these surfaces, Novo populates artifact.ref with its own native { kind: 'novo', id } handle instead, so the agent always has a durable reference to hand off rather than an expiring signed URL.
Generated media and explicit workspace_export_artifact calls send the same top-level fields, but content is structured:
type ManagedArtifactContent =
| { kind: 'base64'; base64: string; mimeType: string; [key: string]: unknown }
| { kind: 'url'; url: string; mimeType: string; novoArtifactId: string; [key: string]: unknown };
content.novoArtifactId is the Novo-managed transfer handle: an opaque, retention-scoped id you can use to fetch the bytes (GET /v1/artifacts/:id, or novo.artifacts.get(id) in the SDK) while you save them. It is not a durable identity: return your own durable artifactId/url in the response, and that is what surfaces back to the agent (for example as export.url on workspace_export_artifact). Return ref or block when the agent should paste a product-specific durable reference instead of a raw storage URL.
Document tools write their output files back to the workspace and return Novo transfer handles for downloads/previews. To promote a document to durable storage, have the agent call workspace_export_artifact({ path }) after it decides the file is ready to hand off.
Idempotency
Novo-Request-Id is stable for the same logical artifact. Deduplicate on that header. Return 2xx to acknowledge the export; a non-2xx response, timeout, or invalid JSON makes the current export fail. For oversized tool-result recovery, Novo reports that failure on the data-novo-tool-result-overflow chunk.
Errors
404 result_handler_not_found: bad ID.400 result_handler_invalid_kind: onlyartifactis supported today.400 result_handler_secret_missing: registration requires a secret.