Function Routines
A Function Routine — called Functions in the UI — runs a JavaScript function you define. There is no AI reasoning involved: Functions are deterministic code that executes on a schedule or trigger, runs fast, and costs nothing. You write the logic; Onnie runs it.
Functions are free and unlimited on every plan.
What a Function Routine does
When a Function Routine fires, Onnie executes your JavaScript function in a sandboxed environment with a fixed timeout (up to 30 seconds). Your function receives an input object — built from the input_template you configure on the Routine — and has access to the Platform SDK for interacting with your workspace.
Functions call Supabase directly for reads and writes. They do not go through the engine's AI dispatch layer. That means reads and mutations are fast, consistent, and do not consume AI credits — they are plain database operations against the same database your workspace uses.
When your function returns a value, it is recorded in the run output. You can inspect it in the Routine's Run History tab. If your function throws an error, the run is marked failed and the error message and stack trace are stored for review.
Functions are also usable as steps inside Chain Routines. The same workspace function library is shared — a function you write for a standalone Routine can be referenced in a Chain step, and vice versa.
The Platform SDK surface
Inside your function, the Platform SDK gives you access to these capabilities:
Records — read records from any table with filtering and pagination (sdk.records.read), read a single record by id (sdk.records.get), create new records, update fields on existing records, and delete records. Reads and writes go directly to Supabase with the workspace editor role.
Tasks — create Tasks with a name, body, project, priority, start date, and due date. Assign Tasks to a workspace user or to an Onniebot. Update Task status. Add comments to a Task. The standard pattern: your Function does the data work, then hands off to the AI layer by creating a Task assigned to an Onniebot.
Pages — read the markdown content of a page by id. Full page write support is planned for a future release.
External HTTP — make outbound requests to any URL via sdk.fetch(). Same signature as the standard Fetch API. Use this to call Slack webhooks, push to external services, pull data from APIs without a built-in connector, or send to Zapier if you're bridging to a legacy automation.
Email — send email from your function via the workspace's configured email identity using sdk.email.send(). Provide a recipient, subject, and body (plain text or HTML).
Variables — read workspace variables (including secrets) by key with sdk.variables.get(key). In Chains, write a computed value back to a workspace variable with sdk.variables.set(key, value).
A minimal function looks like this:
export default async function handler(input, sdk) {
const records = await sdk.records.read(input.tableId, { limit: 100 })
for (const record of records) {
if (record.fields.status === 'needs_review') {
await sdk.tasks.create({
name: `Review: ${record.fields.name}`,
projectId: input.projectId,
priority: 'normal',
})
}
}
return { reviewed: records.filter(r => r.fields.status === 'needs_review').length }
}
Writing your function
Navigate to Routines → New Routine → choose Function. You'll land in the Function editor.
Select or create a workspace function. Functions live in the workspace function library (workspace_functions table) and are reusable across multiple Routines. If you're writing a new function, click New function and give it a name and slug. If you want to reuse one that already exists, select it from the list.
Write your code. The editor gives you a JavaScript environment with the Platform SDK available. The function must export a default async function that accepts (input, sdk). The return value is serialized as JSON and stored in the run output.
Configure your input template. On the Routine, define an input_template — a JSON object whose values are passed as the input argument at runtime. Use this to parameterize your function:
{
"tableId": "3f7a8b21-...",
"projectId": "{{DEFAULT_PROJECT_ID}}",
"webhookUrl": "{{SLACK_WEBHOOK_URL}}"
}
Plain values are passed as-is. Double-brace values are resolved from workspace variables at runtime. Separating the function from its configuration means you can run the same function code against different tables, projects, or endpoints by creating multiple Routines that reference it with different input templates.
Test your function. To test before activating, use Run now in the Run History panel. This runs the full Routine against your actual workspace data — reads and writes are real. Gate destructive operations behind a condition in your code if you need a safe path during development.
Set a schedule. Configure the trigger on the Routine: daily, weekly, every X days, monthly, or manual-only. The scheduler handles the rest — no external cron setup required.
Activate. Toggle the Routine to Active. The scheduler will fire it at the next matching next_scheduled_at time.
Functions run with workspace editor permissions. A function that deletes records, sends emails, or calls external services will do so unconditionally when it fires. Test carefully before activating, and add conditional guards for destructive operations in your code.
Cost
Function execution is free and unlimited on every plan. There is no per-run credit charge and no execution cap. Write as many functions as you need, schedule them as frequently as you want.
The per-function sandbox limit is the only constraint: maximum 30-second timeout (timeout_ms between 1,000 and 30,000). A function that exceeds its timeout is terminated and the run is marked failed.
Longer per-function timeouts on Pro and Business plans are planned; until they ship, every function step is capped at 30 seconds.
If your job needs AI reasoning at any step — summarizing, classifying, deciding — use an Agent Routine or a Chain instead. Functions are the right choice for deterministic work: transformations, API calls, notifications, and data pipelines where you know exactly what should happen.
Example: post a Slack message when Tasks are completed
This function queries for Tasks completed in the last 24 hours and posts a digest to a Slack webhook.
export default async function handler(input, sdk) {
const yesterday = new Date(Date.now() - 86_400_000).toISOString()
const tasks = await sdk.tasks.query({
projectId: input.projectId,
status: 'completed',
completed_after: yesterday,
})
if (tasks.length === 0) {
return { sent: false, reason: 'no completed tasks' }
}
const lines = tasks.map(t => `• ${t.name}`).join('\n')
const text = `*${tasks.length} task(s) completed yesterday:*\n${lines}`
await sdk.fetch(input.slackWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
})
return { sent: true, count: tasks.length }
}
Configure the Routine's input_template:
{
"projectId": "<your-project-uuid>",
"slackWebhookUrl": "{{SLACK_WEBHOOK_URL}}"
}
Store the webhook URL as a workspace variable named SLACK_WEBHOOK_URL with the Secret toggle on. The double-brace syntax resolves the variable at runtime; the plaintext URL is injected into the function's input object and is never logged.
Schedule the Routine to run daily at 09
. Each morning, your team gets a Slack summary of what was finished the day before — with no Zapier, no separate service, and no credit cost.