# Writing Scripts

**Page:** kit/exec/writing-scripts

[Download Raw Markdown](./kit/exec/writing-scripts.md)

---

# Writing Scripts

**No boilerplate. No exports. No imports.** Write your logic, return a value — Hoody Exec handles everything else. Every parameter you need is automatically injected into your script context.

---

## Basic Script Structure

A Hoody Exec script is a plain TypeScript/JavaScript file. Magic comments at the top configure behavior. The rest is your code. Return a value and it becomes the HTTP response.

```typescript
// scripts/1/api/users/[id].ts
// @mode worker
// @cors reflective
// @timeout 5000

// ALL parameters automatically available:
// req, res, metadata, shared, console, require, ws
// (mainResult is additionally available only in post.js middleware)

const { id } = metadata.parameters; // Dynamic route param
const user = await fetchUser(id);

// Return value auto-formatted as JSON
return { user };
```

**Key points:**
- **No `export default`** — parameters are injected automatically
- **No `module.exports`** — just write code at the top level
- **No imports needed** for built-ins — `crypto`, `fs`, `path`, `$`, `Database` are pre-injected
- **Magic comments** go at the very top, before any code
- **Return** a value to send it as the response (or use `res` for full control)



**Pattern 1: Direct return** (recommended for simple scripts)
```javascript
// @mode serverless
return { hello: 'world', time: Date.now() };
```

**Pattern 2: Module exports** (for scripts needing the full handler signature)
```javascript
// @mode serverless
module.exports = async (req, res, metadata, shared) => {
  return { hello: 'world', time: Date.now() };
};
```

Both patterns work identically. The direct return pattern is simpler and recommended for most scripts.


---

## Automatically Available Variables

**Every script automatically receives these parameters — no imports, no configuration needed:**

<div style="margin: 1.5rem 0; padding: 1.25rem; background: var(--sl-color-bg-sidebar); border: 2px solid var(--sl-color-hairline); border-radius: 8px;">

### Core HTTP Objects

```typescript
// req - Incoming HTTP request
req.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

```typescript
// res - HTTP response (for full control)
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ success: true }))
res.statusCode = 404  // Set status code
res.setHeader('X-Custom', 'value')
```

### Metadata Object

```typescript
// metadata - Request context and routing info
metadata.executionId      // Unique execution ID
metadata.parameters       // { id: '123' } from dynamic routes like [id].ts
metadata.clientIp         // REAL client IP (see below)
metadata.path             // '/api/users/123'
metadata.method           // 'GET', 'POST', etc.
metadata.url              // Full URL
metadata.query            // Parsed query string { search: 'term' }
```

### State & Tools

```typescript
// shared - State object (persists in worker mode, resets in serverless)
shared.cache = new Map()       // Worker: persists across requests
shared.requestCount = 0        // Serverless: reset every request

// console - Logger
console.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 present
const lodash = require('lodash')    // Auto-installed if not present
```

### WebSocket Context (`ws`)

Available when `// @websocket` is enabled (worker mode only). Provides full control over WebSocket connections:

```typescript
// Event handlers — Direct assignment pattern
ws.open = (socket, req) => { ... }
ws.message = (socket, data) => { ... }
ws.close = (socket, code, reason) => { ... }
ws.error = (socket, error) => { ... }

// OR Event emitter pattern
ws.on('open', (socket, req) => { ... })
ws.on('message', (socket, data) => { ... })
ws.on('close', (socket, code, reason) => { ... })
ws.on('error', (socket, error) => { ... })

// Connection management
ws.connections         // Set of all active WebSocket connections for this hostname
ws.broadcast(data)     // Send data to ALL connected clients
ws.broadcast(data, excludeSocket)  // Send to all except one client

// Socket data — available on each socket instance
socket.data.ip           // Client IP address
socket.data.url          // Request URL
socket.data.headers      // Request headers
socket.data.parameters   // Dynamic route parameters
socket.data.executionId  // Unique execution ID for this connection
```

### Post Middleware Result (`mainResult`)

```typescript
// mainResult - ONLY available in post.js middleware
// Contains the return value of the main script that just executed
// Use it to wrap, transform, or log responses

// post.js example:
return {
  data: mainResult,
  timestamp: Date.now(),
  requestId: metadata.executionId
};
```

### AI Helpers (when `@ai` enabled)

```typescript
// Available when // @ai true is set — no imports needed
ai              // 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 schema
openai          // Pre-configured OpenAI SDK client instance
model           // Pre-configured Vercel AI SDK model (from @ai-model or default)
generateText    // Vercel AI SDK — generate text completions
streamText      // Vercel AI SDK — stream text responses
generateObject  // Vercel AI SDK — generate structured JSON objects
```

See [AI-Powered Scripts](/kit/exec/ai-integration/) for full usage examples and model configuration.

</div>

---

## Response Helpers

The `res` object is enhanced with Express-like convenience methods:

```typescript
res.json({ data: 'value' })       // Send JSON response
res.send('text')                   // Send text response
res.html('<h1>Hello</h1>')        // Send HTML response
res.redirect('/new-path')         // HTTP redirect
res.stream(readableStream)        // Stream response
res.status(404)                   // Set status code (chainable)
```

**Example with chaining:**
```typescript
// Return a 201 JSON response
res.status(201).json({ created: true, id: 'abc123' });
```

---

## Bun Globals & Node.js Built-ins

These are pre-injected into every script — no `require` or `import` needed:

```typescript
// $ - Bun.$ for shell commands
const 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 module
fs.readFileSync('/path/to/file')   // fs module
path.join('/home', 'user')         // path module
// Also available: http, https, net, tls, child_process
```


`$` (Bun shell) executes commands on the container. Use it for system tasks, but be mindful of security — never pass unsanitized user input to shell commands.


---

## Return Anything

**Return whatever you want** — Hoody Exec automatically handles content types, serialization, and status codes.

```javascript
// Return Object → Auto-formatted JSON (Content-Type: application/json)
return { users: [...], count: 42 };

// Return Array → Auto-formatted JSON array
return [{ id: 1 }, { id: 2 }];

// Return String (HTML detected) → Content-Type: text/html
return "<!DOCTYPE html><html><body>Hello</body></html>";

// Return String (other) → Content-Type: text/plain
return "Plain text response";

// Return Number → JSON number
return 42;

// Return Boolean → JSON boolean
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 details
return 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:
```javascript
res.writeHead(200, { 'Content-Type': 'application/xml' });
res.end('<?xml version="1.0"?><data>Custom</data>');
```

**The abstraction**: You focus on logic. Hoody Exec handles HTTP protocol details automatically.

---

## Real Client IPs



**`metadata.clientIp` contains the REAL client IP address** — not the proxy IP.

Unlike traditional setups where you manually parse `X-Forwarded-For` headers, Hoody Exec reads the `x-forwarded-for` header set by the Hoody Proxy and exposes the real client IP directly on `metadata.clientIp`:

```javascript
// CORRECT - Just use it directly
const clientIp = metadata.clientIp;  // 203.0.113.50 (real user IP)
```

**This works for rate limiting, geolocation, access control, analytics — everything.**

See [Hoody Proxy — Real Client IPs](/foundation/proxy/#real-client-ips-zero-configuration) for technical details.



---

## Pre-installed Packages

These npm packages are bundled and always available — 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 |
| `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 (Chromium/Firefox/WebKit) — bring your own browser |
| `puppeteer-core` | Headless Chrome/Firefox automation — bring your own browser |
| `qrcode` | QR code generator |
| `rss-parser` | RSS/Atom feed parser |
| `sanitize-html` | HTML sanitizer |
| `uuid` | UUID generation |
| `ws` | WebSocket client/server |
| `xml2js` | XML ↔ JS object parser |
| `yaml` | YAML parser |
| `zod` | Schema validation (also exposed as `z`) |

Any other npm package is auto-installed on first `require()` — no `package.json` needed.

---

## Script Storage Paths

Scripts are stored in instance-specific directories:

```
/hoody/storage/hoody-exec/scripts/default/1/     # exec-1 (under default subdomain)
/hoody/storage/hoody-exec/scripts/default/2/     # exec-2
/hoody/storage/hoody-exec/scripts/default/test/  # exec-test
# Runtime routing also accepts the execId-only fallback /hoody/storage/hoody-exec/scripts/1/
```

**The instance number** (`1`, `2`, `test`, etc.) maps to the hostname:
- `exec-1` → `scripts/1/`
- `exec-2` → `scripts/2/`
- `exec-test` → `scripts/test/`

**To create scripts programmatically**, use the [`scripts/write` endpoint](/api/exec/script-management/):

```bash
curl -s -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/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
  }'
```


When creating exec scripts programmatically (from AI agents, other services, automation), **always use the `scripts/write` endpoint** — never write directly via hoody-files. The `scripts/write` endpoint validates magic comments, checks syntax, and places files in the correct instance directory.


---

## Companion System Prompt Files

When using AI-powered scripts (`// @ai true`), you can define system prompts in companion markdown files. These are loaded automatically and injected into the AI context.

### Script-Level System Prompt

Create a `.system.md` file matching your script name:

```
scripts/1/
├── chat.js            # Your AI script
├── chat.system.md     # System prompt for chat.js (loaded automatically)
├── summarize.js
└── summarize.system.md
```

The file `chat.system.md` is automatically loaded when `chat.js` executes. Write your system prompt as plain markdown:

```markdown
You are a helpful coding assistant. Be concise and provide working code examples.
Always explain your reasoning step by step.
```

### Directory-Level Default System Prompt

Create a `_system.md` file to set a default system prompt for all AI scripts in that directory:

```
scripts/1/api/
├── _system.md         # Default system prompt for all scripts in api/
├── chat.js            # Uses _system.md (no script-level override)
├── chat.system.md     # Overrides _system.md for chat.js specifically
└── translate.js       # Uses _system.md
```

**Resolution order**: Script-level (`chat.system.md`) takes precedence over directory-level (`_system.md`).

See [AI-Powered Scripts](/kit/exec/ai-integration/) for full details on AI configuration and model selection.

---

## Powered by Bun

Hoody Exec runs on the [Bun](https://bun.sh) runtime:

- **3x faster startup** — Native speed optimizations
- **Modern JavaScript** — Latest ECMAScript features built-in
- **Better module system** — Improved dependency handling
- **Optimized for serverless** — Perfect for fast script execution
- **Lower memory usage** — More efficient runtime

---

## What's Next