Best Practices
Idempotency, error handling, pagination, and documentation conventions
Scripting Best Practices
Idempotency
All automation steps must be safe to retry. If a step fails halfway through and the platform retries it, it must not create duplicate records or corrupt state.
export async function run(input: Record<string, any>, ctx: StepContext) {
const orderId = input.order_id;
// 1. Check if already processed
const existing = await ctx.stores.processed_orders.find_one({ order_id: orderId });
if (existing) {
ctx.log("already processed, skipping", { order_id: orderId });
return { status: "skipped" };
}
// 2. Perform the action
// ...
// 3. Record success
await ctx.stores.processed_orders.insert({
order_id: orderId,
processed_at: "2026-06-04T00:00:00Z",
});
return { status: "success" };
}Error Handling
Never swallow exceptions. Let them propagate so the platform can track failures and trigger retries.
// ✅ Correct — check status and throw
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`);
}
// ❌ Wrong — silent swallow
try {
// ...
} catch (e) {
// empty catch — never do this
}Always define an on_failure step in integration.yaml for critical steps:
- id: process
name: Process Record
description: Main processing logic
file: process.ts
on_failure: handle_error
- id: handle_error
name: Handle Error
description: Logs failure and sends alert
file: handle_error.ts
input:
error_message: "${{ error.message }}"
failed_step: "${{ error.step }}"Timeouts
Always set explicit timeouts. The default step timeout is 30 seconds.
const response = await ctx.http.get("https://api.example.com/data", {
timeout: 25000, // leave headroom below the 30s step limit
});Pagination
Never assume a single API call returns all records. Always paginate:
// ✅ Correct — paginate until exhausted
let allRecords: any[] = [];
let page = 1;
while (true) {
const response = await ctx.http.get(`https://api.example.com/records?page=${page}&limit=100`, {
headers: { Authorization: `Bearer ${ctx.credentials.API_KEY}` },
timeout: 25000,
});
const data = await response.json();
const batch = data.records || [];
if (batch.length === 0) break;
allRecords = allRecords.concat(batch);
page++;
}
// ❌ Wrong — hard-coded limit may silently truncate
const response = await ctx.http.get("https://api.example.com/records?limit=250", { timeout: 25000 });Never use a round-number limit (100, 250, 500) as a final fetch without verifying the total count.
Incremental Development
Never write and run a complex script in one shot without probing the target system first:
- Fetch ONE record and log its raw structure
- Write transformation logic for that one record, run again
- Expand to the full dataset only after step 2 succeeds
export async function run(input: Record<string, any>, ctx: StepContext) {
// Probe step — log raw structure first
const response = await ctx.http.get("https://api.example.com/records?limit=1", {
headers: { Authorization: `Bearer ${ctx.credentials.API_KEY}` },
timeout: 25000,
});
const records = await response.json();
ctx.log("sample record", { record: records[0] });
return { sample: records[0] };
}knowledge_base.md Conventions
Include a knowledge_base.md in your integration package to document context for future developers and agents:
# knowledge_base.md — my-integration
## Purpose
What this integration does and why.
## API Schema
Document field names, types, and constraints discovered from the target API.
## Credentials Used
- `MY_API_KEY` — Bearer token for api.example.com
## Known Limitations
- The API returns at most 500 records per page
- Rate limit: 100 requests/minutefix_log.md Conventions
When fixing an error, create or update fix_log.md in the integration package:
# Fix Log — my-integration
## 2026-06-04 — Schema mismatch on contact_name field
- **Issue**: Step failed with TypeError: undefined is not an object (run_abc123)
- **Root Cause**: The CRM API changed the field from `contact_name` to `full_name`
- **Resolution**: Updated `process.ts` to use `data.full_name || data.contact_name`
- **Status**: Resolved & VerifiedTemplate Expressions
Dynamic value interpolation in integration.yaml using ${{ }} syntax
npm Packages
Install any npm package into your HeadlessOps integration's execution environment. Declare dependencies in integration.yaml using the packages field, or via the requirements field through the REST API or MCP. Packages are cached for zero-overhead reinstalls on repeat runs.