Action endpoints
Serve tools, hooks, and flow functions over HTTP; the Runtime calls them.
Your tools run in your app, behind one HTTP route. The Runtime POSTs each
Action (a tool call, a hook, or a flow fn, verify, or tool step) to
the URL your app registers, and records the answer as the Action's outcome.
That URL is the agent's Action endpoint.
The generated src/main.ts serves one:
import { createServer } from "node:http";
import { createActionHandler } from "@nylorun/agents";
import { agents } from "../agents/index.js";
const port = Number(process.env.PORT ?? 3001);
const url = process.env.NYLORUN_ACTIONS_URL ?? `http://localhost:${port}/nylorun/actions`;
const actions = createActionHandler({ agents, url });
const server = createServer(actions.node);
await new Promise<void>((resolve) => server.listen(port, resolve));
await actions.register({ url });actions.node serves node:http and Express. actions.fetch is a
web-standard handler for Hono, Next.js route handlers, Workers, Bun, and Deno.
register({ url }) saves the definitions, registers the URL for every served
agent, and pings each one through the Runtime, so a wrong URL fails at
startup. Sandbox tools run inside the Runtime instead, so sandbox-only agents
need no endpoint.
Reachability
The Runtime must reach the URL:
- Local stack. The Runtime runs in Docker and maps
localhostto your machine, sohttp://localhost:3001/nylorun/actionsworks. - Production. Register a URL the Runtime's machine can reach, and set
NYLORUN_ACTIONS_URLto it. - Remote Runtime, local app. Use a tunnel such as ngrok or Cloudflare Tunnel.
A Runtime that refuses private addresses refuses localhost URLs. Redirects
are not followed.
A delivery
The Runtime sends POST <url> with:
| Part | Value |
|---|---|
| Body | { "type": "action", "action": {…}, "sandbox": boolean } |
Nylorun-Signature | A delivery token for this Tenant, URL, Action, generation, and exact body. |
Idempotency-Key | The Action id. |
The handler verifies the token before any code runs, then runs your
implementation with the request's signal as context.signal. Deliveries of
one Action never overlap. The Runtime settles the Action from the answer:
| Answer | Result |
|---|---|
200 with Nylorun-Outcome: 1 | The tagged outcome. createActionHandler always answers this way. |
Plain 200 | A tool's output, or what an fn or verify returned. The tool's output schema still applies. |
202 | The result comes later. See Long-running tools. |
429, 503, 409, or never sent | Retried with backoff from 250 ms to 30 s, honouring Retry-After. Reported as action.delivery_failed. |
Any other 4xx, or a 3xx | The Action fails with endpoint.rejected. |
No answer after sending: timeout, reset, 5xx | Lost. A tool becomes uncertain; a hook, fn, or verify is delivered again. |
The handler answers 404 for an agent or tool it does not serve, 409 for a
flow Action from another version of the flow, and 503 for a token signed
with a key it has not seen yet, which the Runtime retries.
Credentials
register() uses the application key to call PUT /v1/endpoints. Serving
deliveries needs no key: each delivery carries its own short-lived token,
signed with the Tenant's key. A process that only serves Actions can read the
Tenant's public keys without a credential:
const actions = createActionHandler({
agents,
url: "https://app.example.com/nylorun/actions",
runtime: {
url: process.env.NYLORUN_RUNTIME_URL!,
tenant: process.env.NYLORUN_TENANT!,
},
});Register from a deploy step that holds the application key instead. A delivery token authorizes only its own Action's heartbeat, result, and sandbox calls. It cannot act for a subject and is refused from browsers.
Options
register() takes timeoutMs, how long one inline delivery may take (default
60 s, at most 840 s), and maxConcurrent, the in-flight deliveries per agent
(default 16). implementationVersion defaults to
NYLORUN_IMPLEMENTATION_VERSION, then dev. All options are on
createActionHandler.
Long-running tools
Mark a tool that may outlast timeoutMs with background: true:
const buildReport = tool({
name: "build_report",
input: z.object({ month: z.string() }),
background: true,
async run({ month }, context) {
return await reports.build(month, { signal: context.signal });
},
});The handler answers 202 at once, runs the tool, heartbeats with
POST /v1/actions/:id/heartbeat, and posts the outcome to
POST /v1/actions/:id/result. A heartbeat answered 409 (cancelled, lost, or
delivered again) aborts context.signal, and nothing is posted. A delivery
that stops heartbeating is lost at its deadline. On platforms that end work
with the response, pass waitUntil to createActionHandler.
Check an endpoint
npx @nylorun/cli tenant endpoints # each agent's endpoint and health
npx @nylorun/cli tenant endpoints ping <agent> # send a signed ping through the RuntimeHealth records the last delivery, last success, last error, consecutive
failures, and what the last ping reported. Sessions record each delivery as
action.delivered, and each failed attempt as action.delivery_failed; Studio
shows both.
Retries and uncertain work
Stopping a Worker or closing a Tenant does not cancel the interrupted turn;
the next advance resumes from its checkpoint. Takeover marks in-flight effects
uncertain. Only user cancellation ends the turn as cancelled; it aborts the
request, and because the code may have run, the Action becomes uncertain.
The Runtime never repeats a tool whose delivery was lost. Hooks, fn, and
verify may be delivered again, so keep irreversible side effects in tools
and make them idempotent with context.idempotencyKey.
Replaces executors
Executors, connectAgents, NYLORUN_EXECUTOR_KEY, /v1/executors, claims,
and the action_result command were removed in protocol 3. Mount
createActionHandler and call register({url}) instead. Upgrade the Runtime
and SDK together. See Compatibility.