Operations
The operation contract. Methods, path templates, input binding, agent targets, transforms and declared responses.
An operation is one method and one path, bound to one tool or agent. Everything a caller can send and everything they get back is declared here, and most of it is checked when you save rather than when somebody calls.
A definition that routes ambiguously, or whose path parameters do not match its declared inputs, cannot be saved at all. Those are the mistakes that would otherwise surface as a wrong response or a 500, so they are refused at the point they are made.
Method and safety
safety is what you promise about the operation in HTTP's own terms, and the platform
holds you to it per method.
| Safety | Meaning | Required for |
|---|---|---|
safe | No side effects at all. | GET |
idempotent | Repeating it has the same effect as doing it once. | PUT, DELETE |
unsafe | Neither. The default, and all POST usually needs. | — |
Nothing about a tool reveals which of these it is, so you assert it. Two things depend on the assertion:
- Read-only grants may call only
safeoperations. This is why it is a safety declaration rather than a method allowlist: a search that takes a large body is aPOSThere and is stillsafe. Idempotency-Keyis honoured only for operations that are notsafe. Replaying a read costs a stored row for no benefit.
PUT and DELETE may not be unsafe, because callers and proxies retry them on the
assumption that they are not.
Path templates
/coverage/{employeeId}| Rule | Why |
|---|---|
Starts with /, no trailing /, no empty segment | / alone is the API root |
A segment is a literal or exactly one {param} | No regex, optional segments or multi-segment wildcards |
| Literals are lowercase letters, numbers and hyphens | Readable, and unambiguous against parameters |
| The first segment must be a literal | Keeps the operation recognisable in a log |
| At most two path parameters | Beyond that, a query parameter is clearer |
| Parameter names are unique within the template | A duplicate cannot be bound |
Segments may not start with _ | Reserved for platform paths such as _openapi.json |
Matching is path-first, most specific wins. /coverage/count is tried before
/coverage/{employeeId}, so a literal beats a parameter at the leftmost differing
segment. Once a path matches, the method is looked up within it; if the path exists but the
method does not, the answer is 405 with an Allow header listing that path's methods.
There is never a tie, because two operations on the same method whose templates could
match the same path are refused at save time. A path segment that is not valid
percent-encoding is a 400, not a 404.
Inputs
Inputs are keyed by the target's argument name, so dispatch is a pick, a rename and a coercion with no mapping layer in between.
| Field | Purpose |
|---|---|
in | path, query, header or body. Mirrors OpenAPI's in, minus cookies. |
as | The public name, when callers should use something other than the argument name. |
from | A dot path into the request body, for callers who send nested JSON. Body only. |
default | Used when the caller omits the value. Not allowed together with required. |
required | Refuses the request when absent. |
Where a value may come from
in | Constraints |
|---|---|
path | Always required. Must be string, number or boolean, never an array. The template must have a matching {param}. |
query | May not be an object or an array of objects. |
header | Matched case-insensitively, since Node lower-cases incoming header names. |
body | Not allowed on GET or DELETE, whose semantics forbid a body in practice. |
Template placeholders and declared path inputs must correspond exactly, in both
directions. A {param} with no input, or a path input with no {param}, is refused when
you save rather than left to become a 500.
Coercion
Path, query and header values arrive as strings and are coerced to the declared type. Body values arrive from JSON already typed and are checked rather than coerced.
That asymmetry is deliberate. Accepting the string "42" for a declared number in a
body would mean the same contract behaves differently depending on where the value was
carried.
| Declared | From a string |
|---|---|
number | Must parse finite. An empty or blank value is refused, because Number('') is 0. |
boolean | true, 1, yes / false, 0, no, case-insensitive. |
string | Taken as-is. |
A failed coercion is a 400 naming the field.
Renaming, and reaching into a body
inputs:
employeeId: # the tool's argument name
in: path
as: employee_id # what the caller writes: /coverage/{employee_id}
region:
in: body
from: account.region # caller sends { "account": { "region": "eu" } }
pageSize:
in: query
default: 25Note that as changes the public name everywhere it appears, including in the path
template: the template says {employee_id}, and the tool receives employeeId.
Mapping is limited to those three knobs on purpose. Anything computed belongs in a tool, which is versioned, testable and observable; an expression evaluated during binding would be a second place where behaviour lives.
Targets
An operation points at the logical tool or agent, not at a particular stored revision, and resolves a revision per call.
| Target version | Resolves to |
|---|---|
published | The newest published revision. The default, and almost always right. |
| A version number | That revision, pinned. For an endpoint that must not move. |
latest | The newest definition including an unpublished draft. |
latest is only accepted on an API whose contract version is itself latest. On a
versioned API it is refused, because a caller of v1 has no way to know a draft is
answering them. See Versions.
Agent targets
An agent is asked in words, so an agent operation needs a message template and a tool operation must not have one.
Summarise the coverage for employee ${employeeId} in plain English.Placeholders name the keys of inputs — the argument names, not the public names an
as gives them. A bare identifier that is not a declared input is refused when you save,
because ${employeeld} with a lowercase L would otherwise interpolate as empty and
quietly change the prompt. Qualified expressions such as ${user.email} resolve against
the evaluation context instead and are left unchecked.
A template of exactly ${message} with a declared message input is a passthrough, which
is how a chat-style endpoint is built.
Two further requirements for an agent target:
- It must declare structured output. Without it there is nothing to return.
- A supervisor cannot be a target.
Conversations continue through x-thread-id; see
Calling an endpoint.
Responses
Transform
An optional pipeline runs over the target's output before it is returned. Steps run in order:
| Step | What it does |
|---|---|
extract | Evaluates a JEXL expression against the output, for example data.results. |
arrayToMap | Turns an array into an object keyed by a field of each item. |
A transform is the right place to narrow a large upstream payload to the handful of fields your contract promises.
extract takes an expression, so a mistake does not fail. A wrong path resolves to undefined and
the caller gets an empty 200. Use Preview in the editor against a real sample, which is what
makes this usable rather than merely present.
Declared output schema
An operation may declare a JSON Schema for what it answers with, after any transform. It is used twice:
- In the document, so a generated client is typed rather than handed
any. - At dispatch, where the body is checked against it.
A response that does not match is logged and still sent. A mistake in a declared schema should cost observability, not uptime. That log line is the only thing that notices the contract and the target have drifted apart, so it is worth watching.
Only schemas the platform can actually check are accepted; anything else is refused when you save, rather than looking like a schema that passes when nothing is being checked.
Success status
Any 2xx. A 204 sends no body, which is what lets a delete operation declare one.
Size and time budgets
| Budget | Default | Exceeded |
|---|---|---|
| Response size | 1 MiB | 413 RESPONSE_TOO_LARGE |
| Operation runtime | 30s, per-API override up to 300s | 504 TIMEOUT |
Both are bounds rather than optimisations. An operation returning megabytes is a definition that needs a transform, and an unbounded one costs the caller's time, a full walk in the output-shape check, and a stored row for an idempotent call.