Writing Scripts
Section titled “Writing Scripts”A Hoody Exec script needs no boilerplate, exports, or imports: write your logic and return a value. Hoody Exec turns the return into the HTTP response and injects every parameter you need into the script context.
Basic script structure
Section titled “Basic script structure”A Hoody Exec script is a plain TypeScript or JavaScript file. Magic comments at the top configure behavior; the rest is your code. Return a value and it becomes the HTTP response.
// @mode worker// @cors reflective// @timeout 5000
// All parameters are available automatically:// req, res, metadata, shared, console, require, ws// (mainResult is additionally available only in post.ts middleware)
const { id } = metadata.parameters; // Dynamic route paramconst user = await fetchUser(id);
// Return value auto-formatted as JSONreturn { user };Key points:
- Parameters are injected automatically, so there is no
export default. - You do not need
module.exports: write code at the top level, or use Pattern 2 below. - Built-ins need no imports:
crypto,fs,path,$, andDatabaseare pre-injected. - Magic comments go at the very top, before any code.
- Return a value to send it as the response, or use
resfor full control.
Automatically available variables
Section titled “Automatically available variables”Every script receives these parameters automatically, with no imports or configuration:
Core HTTP objects
Section titled “Core HTTP objects”// req - Incoming HTTP requestreq.url // '/api/users/123'req.method // 'GET', 'POST', etc.req.headers // { 'authorization': 'Bearer ...', ... }req.body // Parsed JSON body (if Content-Type: application/json)Response object
Section titled “Response object”// res - HTTP response (for full control)res.writeHead(200, { 'Content-Type': 'application/json' })res.end(JSON.stringify({ success: true }))res.statusCode = 404 // Set status coderes.setHeader('X-Custom', 'value')Metadata object
Section titled “Metadata object”// metadata - Request context and routing infometadata.executionId // Unique execution IDmetadata.parameters // Merged dynamic-route params + query string, e.g. { id: '123', search: 'term' }metadata.clientIp // Real client IP (see below)metadata.path // '/api/users/123'metadata.method // 'GET', 'POST', etc.metadata.url // Full URLmetadata.query // Alias of metadata.parameters: the same merged object (either name works)State and tools
Section titled “State and tools”// shared - State object (persists in worker mode, resets in serverless)shared.cache = new Map() // Worker: persists across requestsshared.requestCount = 0 // Serverless: reset every request
// console - Loggerconsole.log('message')console.info('info')console.debug('debug')console.error('error')
// require - Module loader (auto-installs missing modules)const axios = require('axios') // Auto-installed if not presentconst lodash = require('lodash') // Auto-installed if not presentWebSocket context (ws)
Section titled “WebSocket context (ws)”Available when // @websocket is enabled (worker mode only). Provides full control over WebSocket connections:
// Event handlers: direct assignment patternws.open = (socket, req) => { ... }ws.message = (socket, data) => { ... }ws.close = (socket, code, reason) => { ... }ws.error = (socket, error) => { ... }
// Or the event emitter patternws.on('open', (socket, req) => { ... })ws.on('message', (socket, data) => { ... })ws.on('close', (socket, code, reason) => { ... })ws.on('error', (socket, error) => { ... })
// Connection managementws.connections // Set of all active WebSocket connections for this hostnamews.broadcast(data) // Send data to all connected clientsws.broadcast(data, excludeSocket) // Send to all except one client
// Socket data: available on each socket instancesocket.data.ip // Client IP addresssocket.data.url // Request URLsocket.data.headers // Request headerssocket.data.parameters // Dynamic route parameterssocket.data.executionId // Unique execution ID for this connectionPost middleware result (mainResult)
Section titled “Post middleware result (mainResult)”// mainResult - only available in post.ts middleware// Contains the return value of the main script that just executed// Use it to wrap, transform, or log responses
// post.ts example:return { data: mainResult, timestamp: Date.now(), requestId: metadata.executionId};AI helpers (@ai, enabled by default)
Section titled “AI helpers (@ai, enabled by default)”// Available by default (set // @ai false to disable); no imports neededai // Helper namespace with three methods: // ai.generate(opts): generate a text completion // ai.stream(opts): stream text chunks // ai.object(opts): generate structured JSON against a schemaopenai // Vercel AI SDK provider factory (createOpenAI); call openai(modelId) for a model // (not the official OpenAI client; for that, require('openai'))model // Pre-configured Vercel AI SDK model (from @ai-model or default)generateText // Vercel AI SDK: generate text completionsstreamText // Vercel AI SDK: stream text responsesgenerateObject // Vercel AI SDK: generate structured JSON objectsSee AI-Powered Scripts for full usage examples and model configuration.
Response helpers
Section titled “Response helpers”The res object is enhanced with Express-like convenience methods:
res.json({ data: 'value' }) // Send JSON responseres.send('text') // Send text responseres.html('<h1>Hello</h1>') // Send HTML responseres.redirect('/new-path') // HTTP redirectres.stream('text/event-stream') // Set streaming headers, then res.write()/res.end() (default text/plain)res.status(404) // Set status code (chainable)Example with chaining:
// Return a 201 JSON responseres.status(201).json({ created: true, id: 'abc123' });Bun globals and Node.js built-ins
Section titled “Bun globals and Node.js built-ins”These are pre-injected into every script, so they need no require or import:
// $ - Bun.$ for shell commandsconst output = await $`ls -la /home/user`.text()const result = await $`echo "Hello"`.text()
// Database - bun:sqlite Database constructor (require('bun:sqlite').Database)const db = new Database('/hoody/databases/app.db')const rows = db.query('SELECT * FROM users').all()
// Node.js built-ins (pre-injected)crypto.randomUUID() // crypto modulefs.readFileSync('/path/to/file') // fs modulepath.join('/home', 'user') // path module// Also available: http, https, net, tls, child_processReturn values
Section titled “Return values”Return any value and Hoody Exec handles the content type, serialization, and status code:
// Return Object → Auto-formatted JSON (Content-Type: application/json)return { users: [...], count: 42 };
// Return Array → Auto-formatted JSON arrayreturn [{ id: 1 }, { id: 2 }];
// Return String (HTML detected) → Content-Type: text/htmlreturn "<!DOCTYPE html><html><body>Hello</body></html>";
// Return String (other) → Content-Type: text/plainreturn "Plain text response";
// Return Number → Content-Type: text/plain (stringified, e.g. "42")return 42;
// Return Boolean → Content-Type: text/plain (stringified, e.g. "true")return true;
// Return Buffer → Auto-detected MIME type (images, PDFs, files)return fs.readFileSync('/path/to/image.png'); // Automatic image/png
// Return Error → 500 status with error detailsreturn new Error("Something went wrong");
// Return Nothing → Empty 204 response// (no return statement or return undefined)For full control, use res directly to bypass auto-handling:
res.writeHead(200, { 'Content-Type': 'application/xml' });res.end('<?xml version="1.0"?><data>Custom</data>');Real client IPs
Section titled “Real client IPs”Pre-installed packages
Section titled “Pre-installed packages”These npm packages are bundled and always available, with no installation delay on first use:
| Package | Description |
|---|---|
@ai-sdk/openai | Vercel AI OpenAI provider |
ai | Vercel AI SDK |
axios | HTTP client |
cheerio | HTML parser (jQuery-like) |
cookie | Cookie parser; v2 API: parseCookie, parseSetCookie, stringifyCookie, stringifySetCookie |
dayjs | Date library |
ejs | Template engine |
jsonwebtoken | JWT creation/verification |
lodash | Utility library |
marked | Markdown parser |
mime-types | MIME type detection |
openai | Official OpenAI SDK |
papaparse | CSV parser |
playwright-core | Browser automation (no bundled browser) |
puppeteer-core | Headless Chrome automation (no bundled browser) |
qrcode | QR code generator (PNG, SVG, data URL) |
rss-parser | RSS/Atom feed parser |
sanitize-html | HTML sanitizer |
uuid | UUID generation |
ws | WebSocket client/server |
xml2js | XML parser and builder |
yaml | YAML parser |
zod | Schema validation (also exposed as z) |
Any other npm package is auto-installed on first require(); there is no package.json to maintain.
Script storage paths
Section titled “Script storage paths”Scripts are stored in instance-specific directories:
/hoody/storage/hoody-exec/scripts/default/1/ # exec-1/hoody/storage/hoody-exec/scripts/default/2/ # exec-2/hoody/storage/hoody-exec/scripts/default/test/ # exec-testThe instance number (1, 2, test, and so on) maps to the hostname:
exec-1→scripts/default/1/exec-2→scripts/default/2/exec-test→scripts/default/test/
The default segment is the subdomain namespace, and it is the literal string default for a hostname with no subdomain in front of the project ID. A request to myapp.PROJECT-CONTAINER-exec-1... resolves under scripts/myapp/1/ instead.
To create scripts programmatically, use the scripts/write endpoint. The path you send is relative to the instance directory, so the request below writes /hoody/storage/hoody-exec/scripts/default/1/api/hello.ts:
curl -s -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d '{ "path": "api/hello.ts", "content": "// @mode serverless\nreturn { hello: \"world\" };", "createDirs": true, "validate": true }'Bun runtime
Section titled “Bun runtime”Hoody Exec runs on the Bun runtime. Bun starts about 3x faster and uses less memory, supports the latest ECMAScript features, and improves module and dependency handling; the fast startup suits serverless script execution.