Skip to content
Hoody.com

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.


Each script runs in one of two execution modes, and that choice determines most of how it behaves: whether state persists, whether WebSocket is available, and when its VM starts.

FeatureWorker modeServerless mode
StatePersistent shared objectNone (fresh each time)
WebSocketSupportedNot available
ConcurrencyUnlimited by default (@concurrent caps it)Unlimited by default (@concurrent caps it)
StartupOnce (fast subsequent requests)Per request (slight overhead)
MemoryHigher (persistent VM)Lower (ephemeral)
Use caseReal-time, stateful APIsWebhooks, isolated tasks

Worker mode (// @mode worker) keeps a persistent VM running: state is shared across requests, WebSocket is supported, and there is no cold start.

Serverless mode (// @mode serverless, also the default when the comment is omitted) starts a fresh VM for every request, in the style of AWS Lambda or Vercel Functions: complete isolation and configurable concurrency.

See Execution Modes for the full comparison with worked examples.


  • Files as endpoints: creating a file creates an HTTP endpoint, with no server configuration.
  • Two modes: worker (persistent) or serverless (isolated).
  • Magic comments: configure scripts with // @mode worker, // @cors *, // @timeout 5000.
  • File-based routing: api/users/[id].ts serves /api/users/123 with 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 false to disable them.
  • Dependency auto-install: require('axios') installs the package automatically, with no package.json.
  • Templates: scaffold scripts from built-in or custom templates.
  • Code validation: TypeScript checking, syntax validation, and dependency analysis.
  • Shared state: an in-memory KV store shared across requests (worker mode only).
  • WebSocket support: real-time bidirectional communication (worker mode only).
  • Monitoring: performance metrics, log streaming, and cache stats.

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/hello

It needs no imports, exports, or configuration. The parameters (metadata, req, res, shared) are injected automatically.


Each Hoody Exec instance is reachable at its own hostname:

{projectId}-{containerId}-exec-{execId}.{server}.containers.hoody.com
ComponentDescriptionExample
projectIdYour project IDabc123
containerIdContainer IDdef456
execIdExec instance identifier1, 2, test
serverServer locationnode-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 directory beneath it:

  • exec-1 serves scripts from /hoody/storage/hoody-exec/scripts/default/1/
  • exec-2 serves scripts from /hoody/storage/hoody-exec/scripts/default/2/
  • exec-test serves scripts from /hoody/storage/hoody-exec/scripts/default/test/

The default segment is the subdomain namespace, and it is the literal string default whenever the hostname carries no subdomain in front of the project ID. Adding one shifts the directory: myapp.{projectId}-{containerId}-exec-1... serves from scripts/myapp/1/.

You can run multiple exec instances on a single container, each with its own scripts, routing, and shared state. Subdomains resolve automatically, so a new exec ID is routable the moment you create it.

Use the exec list endpoint to see which exec instances are available:

Terminal window
# List all exec instance IDs
hoody exec namespaces list -c CONTAINER_ID -o json

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:

Terminal window
# List all scripts in the current exec instance
hoody exec scripts list -c CONTAINER_ID -o json

Every script is an HTTP endpoint, so any script can call any other script with fetch():

scripts/default/1/api/aggregate.ts
// @mode serverless
// Call other exec scripts by their URL path
const 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. To call scripts on a different instance or container, use the full hostname:

// Call a script on exec instance 2
const res = await fetch("https://PROJECT-CONTAINER-exec-2.SERVER.containers.hoody.com/api/users");
// Call a script on a different container
const res2 = await fetch("https://PROJECT-OTHER_CONTAINER-exec-1.SERVER.containers.hoody.com/reports/daily");

Get your first script running in three steps:

1. Write a script file:

scripts/default/1/hello.ts
// @mode serverless
return { message: 'Hello, world!', time: Date.now() };

Or create it programmatically:

Terminal window
# Write a script to the exec instance
hoody exec scripts write -c CONTAINER_ID --path "hello.ts" \
--content "// @mode serverless\nreturn { message: 'Hello, world!' };" \
--create-dirs

2. Execute the script by requesting its URL:

Terminal window
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/hello"

3. Verify the response and logs:

{ "message": "Hello, world!", "time": 1708700000000 }

Stream logs in real time to debug:

Terminal window
# Stream logs from the exec instance
hoody exec logs stream -c CONTAINER_ID --file "exec.log"

All endpoints are relative to your exec instance:

https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com

Core execution:

Script management:

Templates:

Validation:

Dependencies:

Routing:

Package management:

Magic comments API:

State and cache:

Logs:

Monitoring:

System:

User OpenAPI:

SDK management:

Custom templates:

Bundled dependencies:


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

WebSocket chat servers, live dashboards, SSE streams, and collaborative tools: worker mode’s persistent VM handles connections efficiently with shared state.

Stripe, GitHub, and Slack webhooks: serverless isolation prevents cross-contamination, and @concurrent false gives serial processing for consistency.

Intercept and control AI requests through the MITM proxy: add safety checks, modify prompts, and track usage. See Hoody AI Intercept & Control.

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.

Admin dashboards, data migration scripts, and reporting endpoints: choose worker mode for speed with caching, or serverless for isolation and safety.

Track user sessions, implement rate limiting, and maintain connection pools: shared state persists across requests with zero overhead.

Iterate quickly with magic comments, test ideas without deployment complexity, and have AI generate boilerplate. Idea to API in seconds.


Use worker mode when you need state or WebSocket, or when you serve high request volume with caching. Use serverless mode when you need isolation, process webhooks, or have sporadic traffic.

Declare @mode first. Set reasonable timeouts to prevent hangs, use @cors reflective during development, and enable a logging level suited to your debugging. See the Magic Comments reference.

Never run untrusted code in worker mode, where shared state can be contaminated. Put sensitive operations behind authentication, use the container firewall in production, and validate all user input.

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.

Use @concurrent 5 to limit parallel executions and prevent overload, or @concurrent false for serial processing of webhooks, and monitor queue depth through the stats API. @concurrent applies in both worker and serverless mode, and to cron fires.

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.

Use worker mode for frequently called endpoints, since it has no cold start. Monitor cache hit rates (should be above 95%), keep scripts small and focused (under 200 lines), and set timeout limits to prevent hangs.


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, isolated tasks, untrusted code, sporadic traffic, and stateless operations. See Execution Modes for the full comparison.

No. WebSocket requires persistent connection handling in a persistent VM, so only worker mode supports it.

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.

By default a worker handles unlimited concurrent requests in its shared VM. 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.

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.

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 behavior changes. See Magic Comments.

Yes. Bun transpiles .ts files automatically, and the validation endpoints provide full TypeScript checking, with no configuration needed.

Hoody Exec detects require() calls, checks whether the module is installed, and runs npm install automatically. It caches the install status and uses the latest versions unless you pin them in a package.json.

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.


Cause: No script file matches the URL path. 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.

Cause: The comments are not at the top of the file, or a syntax error prevents parsing. Solution: Place magic comments before any code, including imports. Use the validation endpoint to parse them, check the exact syntax (a space after //), and verify the comment name spelling. See Magic Comments.

Cause: The script runs in serverless mode, or the server restarted. Solution: Shared state requires // @mode worker and is in-memory only, so a restart clears it. Use the SQLite service for persistence. See Execution Modes.

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 WebSocket connections.

Cause: The CORS magic comment is missing. Solution: Add // @cors reflective for development, // @cors * for testing, or a specific origin for production (// @cors https://app.com).

Cause: A long-running operation exceeds the timeout. Solution: Add // @timeout 60000 to raise the limit, or // @timeout 0 for no limit (risky). 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 monitor queue depth.

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.