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 state persists, whether WebSocket is available, and when its VM starts.
| Feature | Worker mode | Serverless mode |
|---|---|---|
| State | Persistent shared object | None (fresh each time) |
| WebSocket | Supported | Not available |
| Concurrency | Unlimited by default (@concurrent caps it) | Unlimited by default (@concurrent caps it) |
| Startup | Once (fast subsequent requests) | Per request (slight overhead) |
| Memory | Higher (persistent VM) | Lower (ephemeral) |
| Use case | Real-time, stateful APIs | Webhooks, 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.
Capabilities
Section titled “Capabilities”- 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].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. - 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.
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 directory beneath it:
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 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.
Available exec IDs
Section titled “Available exec IDs”Use the exec list endpoint to see which exec instances are available:
# 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.ids.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 script count.
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 all 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. 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 serverless\nreturn { 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:
{ "message": "Hello, world!", "time": 1708700000000 }Stream logs in real time to debug:
# Stream logs from the exec instancehoody exec logs stream -c CONTAINER_ID --file "exec.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.log`);curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/v1/exec/logs/stream?file=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.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.comCore execution:
POST /{path}: execute scripts via file-based routes
Script management:
GET /api/v1/exec/scripts/list: list all scriptsGET /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 validationPOST /api/v1/exec/validate/typescript: TypeScript checkingPOST /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 requestsPOST /api/v1/exec/monitor/script-performance: script performanceGET /api/v1/exec/health: health check
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 SDK
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 isolation.
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 persistent VM handles connections efficiently with shared state.
Webhook receivers (serverless mode)
Section titled “Webhook receivers (serverless mode)”Stripe, GitHub, and Slack webhooks: serverless isolation prevents cross-contamination, 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 for isolation and safety.
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 when you need isolation, process webhooks, or have sporadic traffic.
Magic comment strategy
Section titled “Magic comment strategy”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.
Security considerations
Section titled “Security considerations”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.
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 monitor queue depth through the stats API. @concurrent applies in both worker and serverless mode, and to cron fires.
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 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.
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, isolated tasks, untrusted code, sporadic traffic, and stateless operations. See Execution Modes for the full comparison.
Can serverless mode use WebSocket?
Section titled “Can serverless mode use WebSocket?”No. WebSocket requires persistent connection handling in a persistent VM, so only worker mode supports it.
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 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.
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 behavior changes. See Magic Comments.
Can I use TypeScript?
Section titled “Can I use TypeScript?”Yes. Bun transpiles .ts files automatically, and the validation endpoints provide full TypeScript checking, with no configuration needed.
How does auto-install work?
Section titled “How does auto-install work?”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.
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.
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 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.
Shared state not persisting
Section titled “Shared state not persisting”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.
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 WebSocket connections.
CORS errors in the browser
Section titled “CORS errors in the browser”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).
Script times out
Section titled “Script times out”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.
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.