Schedule Management
Section titled “Schedule Management”List, trigger, reload, and inspect history of scheduled script executions on a container.
The schedule subsystem runs on top of the @schedule JSDoc directive. Schedules are auto-discovered by scanning the scripts directory at /hoody/storage/hoody-exec/scripts on a container. The four endpoints on this page let operators:
- list every currently registered schedule with its computed next fire
- reload schedules after scripts are added, removed, or edited
- trigger a specific schedule on demand, bypassing the cron timer
- read the recent fire history (with optional rotated log scanning)
All endpoints live on the exec service on a container hostname of the form {projectId}-{containerId}-exec-1.{server}.containers.hoody.com.
List schedules
Section titled “List schedules”GET /api/v1/exec/schedules/list
Section titled “GET /api/v1/exec/schedules/list”Returns every currently registered @schedule directive with its computed next fire and last fire metadata.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/schedules/list \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.schedules.list();Response 200
Section titled “Response 200”{ "total": 2, "schedules": [ { "scriptPath": "/hoody/storage/hoody-exec/scripts/default/1/cron/nightly-report.ts", "scriptRel": "default/1/cron/nightly-report.ts", "subdomain": "default", "execId": "1", "vmCacheKey": "default/1", "expression": "0 2 * * *", "timeoutMs": 60000, "registeredAt": "2026-01-01T00:00:00.000Z", "nextFire": "2026-01-02T02:00:00.000Z", "lastFireAt": "2026-01-01T02:00:00.000Z", "lastFireStatus": "ok", "lastFireRunId": "f4e3d2c1-b5a6-9780-1234-567890abcdef" }, { "scriptPath": "/hoody/storage/hoody-exec/scripts/default/1/cron/hourly-cleanup.ts", "scriptRel": "default/1/cron/hourly-cleanup.ts", "subdomain": "default", "execId": "1", "vmCacheKey": "default/1", "expression": "@hourly", "timeoutMs": 0, "registeredAt": "2026-01-01T00:00:00.000Z", "nextFire": "2026-01-01T01:00:00.000Z", "lastFireAt": null, "lastFireStatus": null, "lastFireRunId": null } ]}The request could not be parsed (for example, a malformed query string).
{ "error": "Malformed query string", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. | Percent-encoding failed to decode. Use valid %XX escapes. |
The rate limit was exceeded.
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: "Too many distinct clients", "Too many requests". |
The server encountered an unexpected error.
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
The service is temporarily unavailable.
{ "error": "Service unavailable", "code": "ERROR_503", "timestamp": "2026-01-01T00:00:00.000Z"}Schedule history
Section titled “Schedule history”GET /api/v1/exec/schedules/history
Section titled “GET /api/v1/exec/schedules/history”Returns the newest-first NDJSON entries from fires.log for every scheduled fire, with optional filtering by script and timestamp and support for paginating through rotated log files.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
scriptPath | query | string | No | Filter entries to one script. An absolute path must be inside scripts-dir. A relative path is scoped like scripts/read: on a host that names an execId (-exec-<id>) or a subdomain, it is read as <subdomain|default>/<execId>/<path> (<subdomain>/<path> when there is no execId) unless it already starts with that prefix; a default. subdomain counts as none. On any other host it is relative to scripts-dir. A path outside scripts-dir is 400. Optional. |
since | query | string | No | ISO 8601 lower bound on ts. Optional. |
limit | query | integer | No | Max entries to return. Default 100, hard max 1000. |
includeRotated | query | boolean | No | When true, also scan rotated fires.log.* files (slower). |
cursor | query | string | No | Continuation token from a previous truncated response; resumes the backward scan where it stopped. Optional. |
curl "https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/schedules/history?limit=50&includeRotated=true" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.schedules.listHistory({ limit: 50, includeRotated: true });Response 200
Section titled “Response 200”{ "total": 2, "limit": 100, "includeRotated": false, "truncated": false, "entries": [ { "ts": "2026-01-01T02:00:00.000Z", "scriptPath": "default/1/cron/nightly-report.ts", "expression": "0 2 * * *", "runId": "f4e3d2c1-b5a6-9780-1234-567890abcdef", "status": "ok", "durationMs": 1842, "returnPreview": "{\"rows\": 4218}" }, { "ts": "2026-01-01T01:00:00.000Z", "scriptPath": "default/1/cron/hourly-cleanup.ts", "expression": "@hourly", "runId": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "status": "error", "durationMs": 312, "error": "connection refused (db)" } ]}The request could not be parsed (for example, an invalid ISO 8601 timestamp or an out-of-tree scriptPath).
{ "error": "since must be ISO 8601", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: "since must be ISO 8601", "scriptPath is outside scripts directory", "cursor is not a valid history cursor", "Malformed query string" (Percent-encoding failed to decode. Use valid %XX escapes.). |
The rate limit was exceeded.
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: "Too many distinct clients", "Too many requests". |
The server failed to read the history log.
{ "error": "history read failed", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Messages: "history read failed", "Internal server error". Other messages on this status are composed at runtime and are not listed here. |
The service is temporarily unavailable.
{ "error": "Service unavailable", "code": "ERROR_503", "timestamp": "2026-01-01T00:00:00.000Z"}Reload schedules
Section titled “Reload schedules”POST /api/v1/exec/schedules/reload
Section titled “POST /api/v1/exec/schedules/reload”Scans the scripts directory and reconciles the registered schedule set with the current filesystem state. Use this after adding, removing, or editing scripts to pick up @schedule directive changes without restarting the container.
Request body
Section titled “Request body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
dry_run | boolean | No | false | When true, compute the diff against the filesystem but do not apply. Returns the same shape with {added, kept, removed} lists. |
curl -X POST https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/schedules/reload \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"dry_run": true}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.schedules.reload({ dry_run: true });Response 200
Section titled “Response 200”{ "dry_run": true, "added": [ "default/1/cron/nightly-report.ts" ], "kept": [ "default/1/cron/hourly-cleanup.ts" ], "removed": [ "default/1/cron/legacy-job.ts" ], "failed": [ { "path": "default/1/cron/broken-cron.ts", "reason": "invalid cron expression: \"* * *\"" } ]}The request could not be parsed (for example, a malformed query string or invalid JSON body).
{ "error": "Malformed query string", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: "Malformed query string" (Percent-encoding failed to decode. Use valid %XX escapes.), "Invalid JSON body" (Ensure the request body is valid JSON. Example: a backslash followed by a backtick is not a valid JSON escape.), "Request aborted before the body was fully received". Other messages on this status are composed at runtime and are not listed here. | Percent-encoding failed to decode. Use valid %XX escapes. |
The server timed out reading the request body.
{ "error": "Request body read timeout", "code": "ERROR_408", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_408 | Request timeout | The service answers 408 with this code; the message names the cause. |
The request body exceeded the size limit.
{ "error": "Payload too large", "code": "ERROR_413", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_413 | Payload too large | The service answers 413 with this code; the message names the cause. |
The rate limit was exceeded.
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: "Too many distinct clients", "Too many requests". |
The server encountered an unexpected error.
{ "error": "Internal server error", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Other messages on this status are composed at runtime and are not listed here. |
The service is temporarily unavailable.
{ "error": "Service unavailable", "code": "ERROR_503", "timestamp": "2026-01-01T00:00:00.000Z"}Trigger schedule
Section titled “Trigger schedule”POST /api/v1/exec/schedules/trigger
Section titled “POST /api/v1/exec/schedules/trigger”Fires a specific registered schedule immediately, bypassing the cron timer. The endpoint runs the script in its registered context and returns the fire outcome.
Request body
Section titled “Request body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
scriptPath | string | Yes | Path of a script with a valid @schedule directive. An absolute path must be inside scripts-dir. A relative path is scoped like scripts/read: on a host that names an execId (-exec-<id>) or a subdomain, it is read as <subdomain|default>/<execId>/<path> (<subdomain>/<path> when there is no execId) unless it already starts with that prefix; a default. subdomain counts as none. On any other host it is relative to scripts-dir. A path outside scripts-dir is 400. | |
force | boolean | No | false | When true, bypass the @token refusal. Use with care: this fires the script as cron (no token auth). |
curl -X POST https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/v1/exec/schedules/trigger \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"scriptPath": "default/1/cron/nightly-report.ts"}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.exec.schedules.run({ scriptPath: 'default/1/cron/nightly-report.ts' });Response 200
Section titled “Response 200”{ "triggered": true, "scriptPath": "default/1/cron/nightly-report.ts", "runId": "f4e3d2c1-b5a6-9780-1234-567890abcdef", "status": "ok", "durationMs": 1842}The request was malformed or the script cannot be triggered.
{ "error": "scriptPath is required", "code": "ERROR_400", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_400 | Bad request | The service answers 400 with this code; the message names the cause. Messages: "scriptPath is required", "force must be a boolean", "scriptPath must be inside scripts directory", "script path failed containment check", "script has no @schedule directive", "script is not schedulable", "Malformed query string" (Percent-encoding failed to decode. Use valid %XX escapes.), "Invalid JSON body" (Ensure the request body is valid JSON. Example: a backslash followed by a backtick is not a valid JSON escape.), "Request aborted before the body was fully received". |
The trigger is refused because the script requires token authentication.
{ "error": "script has @token — cron trigger bypasses token auth. Pass {force:true} or start the server with `--schedules-trigger-allow-token true` to proceed.", "code": "ERROR_403", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_403 | Forbidden | The service answers 403 with this code; the message names the cause. |
No script was found at the given path.
{ "error": "script not found", "code": "ERROR_404", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_404 | Not found | The service answers 404 with this code; the message names the cause. |
The server timed out reading the request body.
{ "error": "Request body read timeout", "code": "ERROR_408", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_408 | Request timeout | The service answers 408 with this code; the message names the cause. |
The request body exceeded the size limit.
{ "error": "Payload too large", "code": "ERROR_413", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_413 | Payload too large | The service answers 413 with this code; the message names the cause. |
The rate limit was exceeded.
{ "error": "Too many distinct clients", "code": "ERROR_429", "timestamp": "2026-01-01T00:00:00.000Z", "retryAfter": 60}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_429 | Too many requests | The service answers 429 with this code; the message names the cause. Messages: "Too many distinct clients", "Too many requests". |
The trigger could not complete.
{ "error": "registration not found after refresh", "code": "ERROR_500", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_500 | Internal server error | The service answers 500 with this code; the message names the cause. Messages: "registration not found after refresh", "trigger failed", "Internal server error". Other messages on this status are composed at runtime and are not listed here. |
The scheduler is unavailable.
{ "error": "scheduler unavailable", "code": "ERROR_503", "timestamp": "2026-01-01T00:00:00.000Z"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ERROR_503 | Service unavailable | The service answers 503 with this code; the message names the cause. |