NylorunDocsBeta
Get startedBuildRunDeployReferenceMore

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:

src/main.ts
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 localhost to your machine, so http://localhost:3001/nylorun/actions works.
  • Production. Register a URL the Runtime's machine can reach, and set NYLORUN_ACTIONS_URL to 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:

PartValue
Body{ "type": "action", "action": {…}, "sandbox": boolean }
Nylorun-SignatureA delivery token for this Tenant, URL, Action, generation, and exact body.
Idempotency-KeyThe 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:

AnswerResult
200 with Nylorun-Outcome: 1The tagged outcome. createActionHandler always answers this way.
Plain 200A tool's output, or what an fn or verify returned. The tool's output schema still applies.
202The result comes later. See Long-running tools.
429, 503, 409, or never sentRetried with backoff from 250 ms to 30 s, honouring Retry-After. Reported as action.delivery_failed.
Any other 4xx, or a 3xxThe Action fails with endpoint.rejected.
No answer after sending: timeout, reset, 5xxLost. 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 Runtime

Health 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.

Next step

On this page