Script Execution
Section titled “Script Execution”Execute scripts as HTTP endpoints. Every script on the container maps to a hostname and path, and any HTTP method reaches that script at the same URL with the same behaviour (a small set of exceptions is described below). Use this endpoint to call any deployed script by its route, or to read a .md documentation file beside it.
Execute a script
Section titled “Execute a script”GET /{path}
Section titled “GET /{path}”Dispatch a request to the script at path. POST is accepted at the same endpoint with identical behaviour. The script can set its own status with res.status() and its own Content-Type with res.setHeader(); without those, the service picks a Content-Type from the returned value: objects and arrays become JSON, strings starting with HTML markup become HTML, other strings and Buffers become plain text or bytes-by-type, and streams are written as they produce.
A few exceptions apply on every method: OPTIONS is answered 204 by the CORS preflight before any script runs (unless a proxy hook dispatches it); HEAD runs the script but sends no body; PUT, PATCH, DELETE and any other method with a body read the body as POST does, parsed by Content-Type (JSON, form, multipart or raw bytes) unless the script declares @rawBody. A path ending in .md returns that documentation file instead of running a script; beside a script, the file is served under that script’s @enabled and @token. /favicon.ico is answered by the service on an exclusive origin (an .exclusive marker in the deployment directory), else 404. When a proxy hook on this URL dispatches the request, the upstream is reached via metadata.hook.forward(); the hook’s onUpstreamError fallback answers instead when the upstream fails.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
path | path | string | Yes | Script path (supports Next.js-style routing). Each segment is percent-encoded once; a leading / is optional. |
This endpoint accepts no request body.
Response
Section titled “Response”The script ran and produced a body. The Content-Type is chosen by the script when it sets one; otherwise the service picks one from the returned value (JSON, HTML, plain text, markdown, or a binary type are all possible). A path ending in .md instead returns the contents of that documentation file, served under the script’s @enabled and @token when a script sits beside it. A returned Response is forwarded with its own status and headers, less the framing and hop-by-hop ones.
{ "status": "ok", "message": "Webhook processed"}The script returned nothing and set no status of its own. The response carries no body.
The service refused the request before any script ran, or the script returned a value that did not match its declared @return-type while @return-type-mode dev is set in development. Common causes include a malformed URL path, a proxy hook forwarding a path that has . / .. segments (or one that starts with //), a malformed hostname, and a return-type mismatch (Vary: Accept).
{ "error": "Return type validation failed", "details": [ { "path": "return.count", "expected": "number", "actual": "3", "message": "Expected number, got string" } ], "expected": "{ count: number }", "actual": { "count": "3" }}The script is protected by @token and the request did not carry a matching token. The same status can also be returned by the script itself with any body and Content-Type. The service responds with the header WWW-Authenticate: Bearer realm="hoody-exec".
{ "error": "Unauthorized", "message": "This endpoint requires authentication. Provide a valid token via Authorization: Bearer <token> header, X-Token header, ?token= query parameter, or HTTP Basic Auth."}No script answered this path. A disabled script (@enabled false), a .md path with no file, a system-prompt file (<script>.system.md, _system.md), a file beside a disabled script, and a proxy hook whose target path has no script of its own all return 404 too. A request whose Accept names application/json and not text/html receives the JSON envelope shown below (Vary: Accept); others receive an HTML page headed 404 Not Found listing the paths tried in priority order, with up to 3 same-scope suggestions under “Did you mean”.
{ "error": "No script found for /shop/reports/daily. Did you mean /reports/daily?", "code": "ERROR_404", "details": { "code": "SCRIPT_NOT_FOUND", "path": "/shop/reports/daily", "subdomain": null, "execId": null, "hostname": "default", "tried": [ "/hoody/storage/hoody-exec/scripts/default/shop/reports/daily.ts", "/hoody/storage/hoody-exec/scripts/default/shop/reports/daily.js" ], "suggestions": [ "/reports/daily" ] }, "timestamp": "2026-01-01T00:00:00.000Z"}The script could not run to completion: it failed to compile or install its dependencies, it threw an error, it broke its @return-type in strict mode (or in dev mode in production), it returned an Error, or the service failed while handling the request. The HTTP status always stays 500. A script that throws answers with text/plain; charset=utf-8 by default (the plain sentence VM Error in script '<name>': <message>), or with the JSON envelope below when the request’s Accept names application/json and not text/html (Vary: Accept). Only the service’s own failure is sent without a Content-Type.
Compile error (script failed to load):
{ "error": "Script failed to load: Unexpected token ';' (line 3)", "code": "ERROR_500", "details": { "code": "SCRIPT_COMPILE_ERROR", "script": "/hoody/storage/hoody-exec/scripts/default/report.js", "message": "Unexpected token ';'", "line": 3 }, "timestamp": "2026-01-01T00:00:00.000Z"}Script threw (the JSON form, returned when Accept asks for application/json):
{ "error": "VM Error in script 'files.ts': ApiError: File or directory not found", "code": "ERROR_500", "details": { "code": "NOT_FOUND", "upstreamStatus": 404 }, "timestamp": "2026-01-01T00:00:00.000Z"}Returned Error:
{ "error": "Order not found"}Route scope unreadable (exclusive marker storage failed):
{ "error": "Route scope unreadable", "code": "ROUTE_SCOPE_UNREADABLE", "message": "Cannot read the exclusive marker of default/4821907: EACCES"}A proxy hook’s metadata.hook.forward() could not reach the upstream (network error, abort or timeout) before any answer was sent, and the hook script gave no usable onUpstreamError fallback. The response carries an X-Hoody-Hook-Audit header. A hook fallback that returns a status outside 100..599, or one that returns a non-object value, is rewritten to 502 by the service. Scripts can also return this status themselves with any body and Content-Type.
upstream unavailableA time limit ran out. Either the script’s own @timeout ran out (default 60 minutes, 0 disables it, counting wait for a free slot, script load including dependency install, and reading the request body), or a proxy hook’s deadline ran out. The connection is closed and a script that had started may still be running.
Script timeout:
{ "error": "Script timeout", "timeoutMs": 30000}Hook timeout:
{ "error": "hook timeout"}SDK usage
Section titled “SDK usage”Default GET request to a script:
curl "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/scripts/handle-webhook"POST with a JSON body:
curl -X POST "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/scripts/handle-webhook" \ -H "Content-Type: application/json" \ -d '{"event": "push"}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });GET (no options):
await client.exec.run("scripts/handle-webhook");POST with a JSON body:
await client.exec.run("scripts/handle-webhook", { method: "POST", body: { event: "push" } });