Hoody Exec
Section titled “Hoody Exec”Hoody Exec is the recommended way to run scripts on Hoody. Write a TypeScript or JavaScript function in a file and Hoody Exec serves it as an HTTP endpoint, handling routing, JSON serialization, dependency installation, and production-grade execution. You do not install Express or Fastify, configure a web server, or maintain a package.json. This is “everything is HTTP” in practice: the file you write is the API.
Two execution modes
Section titled “Two execution modes”Each script runs in one of two execution modes, and that choice determines most of how it behaves: whether context state persists, whether WebSocket is available, and when its context is built. Neither mode is a security boundary. Both modes run with the Exec service’s own authority: scripts can use the filesystem, spawn subprocesses and reach the network, so both need code you trust. Give mutually untrusted workloads separate containers.
| Feature | Worker mode | Serverless mode |
|---|---|---|
| State | Persistent shared object | Same per-instance shared object; only context-local state resets |
| WebSocket | Supported | Not available for script WebSockets (remote channels excepted) |
| Concurrency | Unlimited by default (@concurrent caps it) | Unlimited by default (@concurrent caps it) |
| Startup | Once (fast subsequent requests) | Per request (slight overhead) |
| Memory | Higher (context stays alive) | Lower (context per request) |
| Use case | Real-time, stateful APIs | Webhooks, one-off tasks |
Worker mode (// @mode worker) keeps one JavaScript context alive per instance (one exec ID), shared by every worker script in that instance: context state is kept across requests, WebSocket is supported, and later requests reuse the cached context. The first request, and requests after a restart or cache eviction, build a new context.
Serverless mode (// @mode serverless, also the default when the comment is omitted) builds a fresh JavaScript context for every request, in the style of AWS Lambda or Vercel Functions: context-local state resets and configurable concurrency. Functions a script registers for remote messages and calls are the exception: they keep the variables of the run that registered them.
See Execution Modes for the full comparison with worked examples.
Capabilities
Section titled “Capabilities”- Files as endpoints: creating a file creates an HTTP endpoint, with no server configuration.
- Two modes: worker (one long-lived context) or serverless (a context per request).
- Magic comments: configure scripts with
// @mode worker,// @cors *,// @timeout 5000. - File-based routing:
api/users/[id].tsserves/api/users/123with dynamic parameters. - Bun runtime: scripts run on Bun, a modern JavaScript runtime faster than Node.js.
- AI helpers:
ai,openai,model,generateText, and more are injected automatically. They are on by default; set// @ai falseto disable them. - Hoody SDK client:
hoodyis injected already pointed at the script’s own container, soawait hoody.files.readText(path)orawait hoody.terminal.run(cmd)works with no login or setup. See Hoody SDK client. - Dependency auto-install:
require('axios')installs the package automatically — maintaining apackage.jsonis optional. - Templates: scaffold scripts from built-in or custom templates.
- Code validation: TypeScript syntax checking, syntax validation, and dependency analysis. With
typecheck: true, Hoody SDK calls are also checked against the SDK version installed, andGET /api/v1/exec/sdk-typeslooks up exact signatures, so an agent finds a wrong method or argument before deploying. See Check SDK calls before they run. - Shared state: an in-memory KV store shared across requests, per instance (hostname and exec ID), in both modes.
- WebSocket support: real-time bidirectional communication (script WebSockets need worker mode; remote channels also work in serverless mode).
- Remote messages and events: a running script publishes events with
remote.emit()to clients listening over SSE or WebSocket on its own URL, and answers messages and calls from them. Off by default; see Remote Messages and Events. - Monitoring: performance metrics, log streaming, and cache stats.
Quick example
Section titled “Quick example”Create scripts/default/1/api/hello.ts:
// @mode serverless// @cors reflective// @timeout 5000
return { message: 'Hello from Hoody Exec!', timestamp: new Date().toISOString(), method: metadata.method, ip: metadata.clientIp};The script is now live at:
https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/helloIt needs no imports, exports, or configuration. The parameters (metadata, req, res, shared) are injected automatically.
URL structure
Section titled “URL structure”Each Hoody Exec instance is reachable at its own hostname:
{projectId}-{containerId}-exec-{execId}.{server}.containers.hoody.com| Component | Description | Example |
|---|---|---|
projectId | Your project ID | abc123 |
containerId | Container ID | def456 |
execId | Exec instance identifier | 1, 2, test |
server | Server location | node-us, node-eu |
The exec ID determines which script directory is served. The full root on the container filesystem is /hoody/storage/hoody-exec/scripts/, and each instance gets its own folder beneath it, which is searched first:
exec-1serves scripts from/hoody/storage/hoody-exec/scripts/default/1/exec-2serves scripts from/hoody/storage/hoody-exec/scripts/default/2/exec-testserves scripts from/hoody/storage/hoody-exec/scripts/default/test/
The default segment is the subdomain folder, and public exec URLs always serve from default. A hostname with an extra name in front, such as myapp.{projectId}-{containerId}-exec-1.{server}.containers.hoody.com, does not reach your container.
You can run multiple exec instances on a single container, each with its own folder and its own shared state. A request is looked up in these folders in order, and the first one with a matching script wins (a script that is a symbolic link is only tried after every regular-file match): scripts/default/<execId>/, then scripts/default/, then scripts/<execId>/, then the scripts root. Scripts placed in default/ or directly in the scripts root are therefore served by every instance. To keep one instance confined to its own folder, add an empty file named .exclusive to it (for example scripts/default/1/.exclusive); that turns the fallbacks off for that instance, and also stops it running any script reached through a symbolic link. See Routing for the full lookup order. Subdomains resolve automatically, so a new exec ID is routable the moment you create it. An exec ID is lowercase letters and digits only, such as 1, test or demoapi: a folder named with a hyphen, underscore or dot (demo-api) is still listed below but is never served.
Available exec IDs
Section titled “Available exec IDs”Use the exec list endpoint to see which exec instances are available in the default namespace:
# List all exec instance IDshoody exec namespaces list -c CONTAINER_ID -o jsonconst containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER});const result = await containerClient.exec.namespaces.list();console.log(result.data.execIds); // [{ id: "1", type: "custom", files: 4 }, ...]curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/v1/exec/list"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists the exec instance IDs available on this container, with each instance’s type and a files count (for custom instances, every file in its directory, including .md and .env files).
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/v1/exec/list&method=GET&response=transparent Example response:
{ "execIds": [ { "id": "1", "type": "custom", "files": 4 }, { "id": "2", "type": "custom", "files": 1 } ], "total": 2, "summary": { "sdk": 0, "custom": 2 }}To list scripts for a specific exec instance:
# List system scripts in the current exec instancehoody exec scripts list -c CONTAINER_ID -o jsonconst scripts = await containerClient.exec.scripts.list();console.log(scripts);curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/list"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Returns the top-level entries of this exec instance’s script directory. Subdirectories come back as entries rather than being expanded; add recursive=true to walk them.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/list&method=GET&response=transparent Script-to-script calls
Section titled “Script-to-script calls”Every script is an HTTP endpoint, so any script can call any other script with fetch():
// @mode serverless
// Call other exec scripts by their URL pathconst news = await fetch("/api/hackernews");const weather = await fetch("/api/weather");
const [newsData, weatherData] = await Promise.all([ news.json(), weather.json()]);
return { news: newsData, weather: weatherData };Relative paths like /api/hackernews resolve to the same exec instance. They need a request to resolve against, so a script run by @schedule must use full URLs. To call scripts on a different instance or container, use the full hostname:
// Call a script on exec instance 2const res = await fetch("https://PROJECT-CONTAINER-exec-2.SERVER.containers.hoody.com/api/users");
// Call a script on a different containerconst res2 = await fetch("https://PROJECT-OTHER_CONTAINER-exec-1.SERVER.containers.hoody.com/reports/daily");First script
Section titled “First script”Get your first script running in three steps:
1. Write a script file:
// @mode serverlessreturn { message: 'Hello, world!', time: Date.now() };Or create it programmatically:
# Write a script to the exec instancehoody exec scripts write -c CONTAINER_ID --path "hello.ts" \ --content "// @mode serverlessreturn { message: 'Hello, world!' };" \ --create-dirsconst containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER});await containerClient.exec.scripts.write({ path: 'hello.ts', content: '// @mode serverless\nreturn { message: "Hello, world!" };', createDirs: true});curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d '{"path": "hello.ts", "content": "// @mode serverless\nreturn { message: \"Hello, world!\" };", "createDirs": true}'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates hello.ts in this exec instance, creating parent directories as needed.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/write&method=POST&json={"path":"hello.ts","content":"//%20@mode%20serverless\nreturn%20{%20message:%20\"Hello,%20world!\"%20};","createDirs":true}&response=transparent 2. Execute the script by requesting its URL:
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/hello"3. Verify the response and logs (the file version above also returns a time field):
{ "message": "Hello, world!" }Stream logs in real time to debug:
# Stream logs from the exec instancehoody exec logs stream -c CONTAINER_ID --file "access/access.log"// Logs are streamed via SSE; use the HTTP endpoint directlyconst res = await fetch( `https://${PROJECT}-${CONTAINER}-exec-1.${SERVER}.containers.hoody.com/api/v1/exec/logs/stream?file=access/access.log`);curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/v1/exec/logs/stream?file=access/access.log"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Streams the log live. The cURL service detects the text/event-stream response and passes it
through unbuffered rather than waiting for a body that never ends, so the link keeps delivering
until you close it or it reaches the 30-minute cap.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/v1/exec/logs/stream?file=access/access.log&method=GET&response=transparent API endpoints summary
Section titled “API endpoints summary”All endpoints are relative to your exec instance:
https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.comEvery /api/v1/exec/ endpoint shares one rate limit: 100 requests per 60 seconds per client. Past it the answer is 429 with retryAfter, the seconds until the window ends, in the JSON body. Requests to your scripts’ own routes are not counted.
Core execution:
POST /{path}: execute scripts via file-based routes
Script management:
GET /api/v1/exec/scripts/list: list scripts and folders at one level (addrecursive=truefor everything below)GET /api/v1/exec/scripts/read: read script contentPOST /api/v1/exec/scripts/write: create or update scriptsDELETE /api/v1/exec/scripts/delete: delete a scriptPOST /api/v1/exec/scripts/move: move or rename a scriptPOST /api/v1/exec/scripts/tree: get the directory tree
Templates:
GET /api/v1/exec/templates/list: list templatesGET /api/v1/exec/templates/preview: preview a templatePOST /api/v1/exec/templates/generate: create from a template
Validation:
POST /api/v1/exec/validate/script: comprehensive validation;typecheck: truealso checks SDK callsPOST /api/v1/exec/validate/typescript: TypeScript transpile check;typecheck: truetype-checks against the installed Hoody SDKGET /api/v1/exec/sdk-types: look up Hoody SDK method signatures (see Check SDK calls before they run)POST /api/v1/exec/validate/syntax: syntax validationPOST /api/v1/exec/validate/dependencies: dependency validationPOST /api/v1/exec/validate/return-type: return type validationPOST /api/v1/exec/validate/magic-comments: parse magic comments
Dependencies:
POST /api/v1/exec/dependencies/check: check for missing packagesPOST /api/v1/exec/dependencies/install: install NPM modules
Routing:
POST /api/v1/exec/route/resolve: resolve which script a URL maps toPOST /api/v1/exec/route/discover: discover all routesPOST /api/v1/exec/route/test: test route matching
Package management:
GET /api/v1/exec/package/read: read package.jsonPOST /api/v1/exec/package/update: update package.jsonPOST /api/v1/exec/package/install: install packagesPOST /api/v1/exec/package/compare: compare packagesPOST /api/v1/exec/package/pin: pin versionsPOST /api/v1/exec/package/init: initialize package.json
Magic comments API:
GET /api/v1/exec/magic-comments/schema: get the magic comments schemaGET /api/v1/exec/magic-comments/read: read a script’s magic commentsPUT /api/v1/exec/magic-comments/update: update magic commentsPOST /api/v1/exec/magic-comments/bulk-update: bulk update magic comments
State and cache:
POST /api/v1/exec/shared-state/get: read shared statePOST /api/v1/exec/shared-state/set: write shared statePOST /api/v1/exec/shared-state/clear: clear shared statePOST /api/v1/exec/cache/clear: clear the execution cache
Logs:
GET /api/v1/exec/logs/list: list logsPOST /api/v1/exec/logs/read: read a logGET /api/v1/exec/logs/stream: stream logs (SSE)POST /api/v1/exec/logs/search: search logsDELETE /api/v1/exec/logs/clear: clear logs
Monitoring:
GET /api/v1/exec/monitor/stats: performance metricsGET /api/v1/exec/monitor/active-requests: active requestsGET /api/v1/exec/monitor/scripts: per-script metrics, recent errors, and VM-cache statePOST /api/v1/exec/monitor/script-performance: script performanceGET /api/v1/exec/monitor/metrics: Prometheus text exposition (404 when Prometheus is off)GET /api/v1/exec/health: health check
Schedules (cron):
GET /api/v1/exec/schedules/list: list registered@scheduledirectives with next and last fireGET /api/v1/exec/schedules/history: past fires and their outcomesPOST /api/v1/exec/schedules/trigger: fire a schedule nowPOST /api/v1/exec/schedules/reload: re-read@scheduledirectives from disk
System:
POST /api/v1/exec/system/restart: restart the serverGET /api/v1/exec/system/restart-status: get restart status
User OpenAPI:
POST /api/v1/exec/user-openapi/generate: generate an OpenAPI spec from user scriptsGET /api/v1/exec/user-openapi/list: list user scripts for OpenAPI generationPOST /api/v1/exec/user-openapi/validate: validate a user schemaGET /api/v1/exec/user-openapi/schema: serve the schema fileGET /api/v1/exec/user-openapi/spec: serve the generated OpenAPI specPOST /api/v1/exec/user-openapi/merge: merge OpenAPI specs
SDK management:
POST /api/v1/exec/sdk/import: import an SDKGET /api/v1/exec/sdk/list: list imported SDKsGET /api/v1/exec/sdk/{id}: get SDK detailsDELETE /api/v1/exec/sdk/{id}: delete an imported SDKGET /api/v1/exec/list: list exec instance IDs in thedefaultnamespace, with each instance’s source type
Custom templates:
POST /api/v1/exec/templates/create-custom: create a custom templatePUT /api/v1/exec/templates/update-custom/{name}: update a custom templateDELETE /api/v1/exec/templates/delete-custom/{name}: delete a custom template
Bundled dependencies:
GET /api/v1/exec/dependencies/bundled: list pre-bundled dependencies
Use cases
Section titled “Use cases”Instant APIs (either mode)
Section titled “Instant APIs (either mode)”Write functions in files instead of setting up Express or Fastify; each file is an HTTP endpoint as soon as it exists. Use worker mode for performance, serverless for sporadic traffic.
Real-time services (worker mode)
Section titled “Real-time services (worker mode)”WebSocket chat servers, live dashboards, SSE streams, and collaborative tools: worker mode’s long-lived context handles connections efficiently with shared state.
Webhook receivers (serverless mode)
Section titled “Webhook receivers (serverless mode)”Stripe, GitHub, and Slack webhooks: a fresh context per request resets context-local state (modules you require or import can still keep state between requests, so keep delivery-specific data out of them), and @concurrent false gives serial processing for consistency.
Hoody AI interception (worker mode)
Section titled “Hoody AI interception (worker mode)”Intercept and control AI requests through the MITM proxy: add safety checks, modify prompts, and track usage. See Hoody AI Intercept & Control.
Proxy hooks (worker mode)
Section titled “Proxy hooks (worker mode)”Register hoody-exec scripts as MITM handlers for any container service (terminal, files, curl, …) directly in your proxy permissions document. The Hoody Proxy dispatches matching traffic through your hook with the real client IP preserved. See Proxy Hooks.
Internal tools (either mode)
Section titled “Internal tools (either mode)”Admin dashboards, data migration scripts, and reporting endpoints: choose worker mode for speed with caching, or serverless when a fresh context per request suits the traffic.
Session management (worker mode)
Section titled “Session management (worker mode)”Track user sessions, implement rate limiting, and maintain connection pools: shared state persists across requests with zero overhead.
Development and prototyping (either mode)
Section titled “Development and prototyping (either mode)”Iterate quickly with magic comments, test ideas without deployment complexity, and have AI generate boilerplate. Idea to API in seconds.
Best practices
Section titled “Best practices”Mode choice
Section titled “Mode choice”Use worker mode when you need state or WebSocket, or when you serve high request volume with caching. Use serverless mode for webhooks and sporadic traffic, where building a context per request costs less than keeping one alive.
Magic comment strategy
Section titled “Magic comment strategy”Declare @mode first. Set a @timeout suited to the endpoint (it answers 504 when nothing has been sent by the deadline; it does not stop a script that has already started or cut off a response that has already started, such as a stream; the deadline also counts time spent loading the script and waiting for a @concurrent slot, and a script that has not started by then never runs (unless pre.ts or pre.js already sent headers, left the response open and returned nothing: then the main script can still start after the deadline). Requests from a proxy hook use the hook’s own deadline instead, which can cut off a response already under way), use @cors reflective during development, and enable a logging level suited to your debugging. See the Magic Comments reference.
Security considerations
Section titled “Security considerations”Never run untrusted code in either mode: both run with the Exec service’s own authority over the filesystem, subprocesses and network, and both see the same per-instance shared object. Give mutually untrusted workloads separate containers. Put sensitive operations behind authentication, use the container firewall in production, and validate all user input.
State management (worker mode)
Section titled “State management (worker mode)”Treat the shared object as an in-memory cache only. Clean up old state periodically to prevent memory leaks, expect state to be lost on restart, and use SQLite for data that must survive restarts.
Concurrency control
Section titled “Concurrency control”Use @concurrent 5 to limit parallel executions and prevent overload, or @concurrent false for serial processing of webhooks, and watch request activity through the monitoring API. @concurrent applies in both worker and serverless mode, to cron fires, and to remote send and call. Messages to an ordinary script WebSocket are not held back by it.
Dependency strategy
Section titled “Dependency strategy”Let auto-install handle common packages without configuration. Pin versions in a package.json when production stability calls for it, test dependencies before deploying, and watch node_modules size growth.
Performance optimization
Section titled “Performance optimization”Use worker mode for frequently called endpoints, since later requests reuse the cached context. Watch request latency through the monitoring endpoints, keep scripts small and focused (under 200 lines), and set @timeout so a request that has received nothing by the deadline gets a 504 (a script that has already started keeps running, one still waiting to start never runs (unless pre.ts or pre.js already sent headers, left the response open and returned nothing: then the main script can still start after the deadline), and a response already under way, such as a stream, is not cut off).
Useful questions
Section titled “Useful questions”When should I use worker vs serverless mode?
Section titled “When should I use worker vs serverless mode?”Use worker mode for WebSocket, stateful apps, high-traffic APIs, session management, and rate limiting. Use serverless mode for webhooks, one-off tasks, sporadic traffic, and operations that keep no context state. See Execution Modes for the full comparison.
Can serverless mode use WebSocket?
Section titled “Can serverless mode use WebSocket?”Not for ordinary script WebSockets: they need persistent connection handling in a long-lived context, so only worker mode supports them. Remote-channel WebSockets (see Remote Messages and Events) also work with serverless scripts.
What happens to shared state on restart?
Section titled “What happens to shared state on restart?”It is lost completely: shared is in-memory only. Use SQLite (the hoody-sqlite service) or an external database for data that must survive restarts.
How does concurrency work in worker mode?
Section titled “How does concurrency work in worker mode?”By default a worker handles unlimited concurrent requests in its shared context. You can cap parallelism with @concurrent N, serialize with @concurrent false (the directive applies in worker mode as well as serverless), or manage concurrency yourself with semaphores or queues for finer control.
Can I mix worker and serverless scripts?
Section titled “Can I mix worker and serverless scripts?”Yes. Each script declares its own mode independently, so a worker script at /api/ws.ts can coexist with a serverless script at /webhooks/stripe.ts.
How do magic comments work?
Section titled “How do magic comments work?”They are parsed at script load time and configure script behavior without code changes. They take precedence over defaults, and the validation API can parse and check them; change a comment and the next request uses it. @schedule, @schedule-timeout and the @timeout a scheduled run falls back to are the exception when you edit the file directly: call POST /api/v1/exec/schedules/reload to apply them (the scripts and magic-comments APIs do this for you). See Magic Comments.
Can I use TypeScript?
Section titled “Can I use TypeScript?”Yes. Bun transpiles .ts files automatically, with no configuration needed. The validation endpoints check TypeScript syntax and transpilation. Use a separate TypeScript checker for type errors.
How does auto-install work?
Section titled “How does auto-install work?”Hoody Exec detects packages that your script names with a plain string in require() or import, checks whether each is installed, and runs npm install automatically. A package named by a computed value, such as require(name), is not detected, so install it first with the package-management endpoints. It caches the install status and uses the latest versions unless you pin them in a package.json. An installed copy older than the version range Hoody Exec lists for one of its always-available packages is reinstalled on use: without a pin that brings the latest version, but a version pinned in the scripts folder’s package.json survives the reinstall, so the older copy stays (a successful reinstall is not repeated until the server restarts). hoody-sdk is installed the same way, and Hoody Exec also keeps it current: it downloads a newer npm release in the background and switches to it when Hoody Exec restarts, unless the scripts folder’s package.json pins the SDK to an exact version (a range keeps updates within that range). A version written into the specifier, such as require('lodash@4.17.21'), fails with ERR_VERSIONED_SPECIFIER; pin it in package.json instead. See Writing Scripts.
Do I need to manage a web server?
Section titled “Do I need to manage a web server?”No. Hoody Exec is the web server: you write functions in files, and the file system is the routing configuration. There is no Express, Koa, or server setup involved.
Troubleshooting
Section titled “Troubleshooting”Script returns 404
Section titled “Script returns 404”Cause: No script file matches the URL path, or the matching script has @enabled false (a disabled script answers 404 and no other script is tried).
Solution: Check that the file exists at /hoody/storage/hoody-exec/scripts/default/1/path/to/script.ts, verify the filename matches the route exactly, and test with the route validation API.
Magic comments not working
Section titled “Magic comments not working”Cause: The comments are not at the top of the file, or a comment name or value is invalid. (A syntax error in the code stops the script from running, but does not stop the comments being read.)
Solution: Place magic comments before any code, including imports, and within the first 30 lines of the file (directives after line 30 are ignored). Use the validation endpoint to parse them, check the exact syntax (a space after //), and verify the comment name spelling. See Magic Comments.
Shared state not persisting
Section titled “Shared state not persisting”Cause: The container restarted, the state was cleared, or the service evicted that hostname’s idle state.
Solution: shared is in-memory in both modes, so any of those empties it for new requests (a WebSocket connection that is still open keeps the old object until it closes). Use the SQLite service for persistence. See Execution Modes.
WebSocket connection fails
Section titled “WebSocket connection fails”Cause: A magic comment is missing, or the script is in the wrong mode.
Solution: The script needs both // @mode worker and // @websocket. Serverless mode cannot serve ordinary script WebSocket connections.
CORS errors in the browser
Section titled “CORS errors in the browser”Cause: Reflective CORS is on by default, so a missing comment is not the cause. A script CORS comment may restrict the origin, or the response or preflight headers may differ from what you expect.
Solution: Check the response and preflight headers in the browser. If the script restricts CORS, use // @cors reflective (// @cors * is an alias for it), or pin a specific origin for production (// @cors https://app.com).
Script times out
Section titled “Script times out”Cause: A long-running operation exceeds the timeout.
Solution: The default limit is one hour (runs started by @schedule use @schedule-timeout if set, otherwise @timeout, otherwise 30 seconds). Raise it with // @timeout, for example // @timeout 2h, or use // @timeout 0 for no limit (risky). Synchronous code that blocks the server for longer than its watchdog limit (30 seconds by default) gets the server restarted, whatever @timeout says. If you set a lower value earlier, such as // @timeout 60000, that is the limit you are hitting. Optimize slow operations and consider async patterns.
Concurrent request limit reached (serverless)
Section titled “Concurrent request limit reached (serverless)”Cause: The @concurrent limit is reached and requests are queuing.
Solution: Increase the limit (// @concurrent 10), optimize script performance, consider worker mode for high traffic, and watch request activity through the monitoring API.
Module not found after auto-install
Section titled “Module not found after auto-install”Cause: The installation failed, or a network issue interrupted it. Solution: Check network connectivity, verify the module exists on npm, read the logs for install errors, check the module name spelling, and install manually if needed.