Automation client
Create, publish, and run automation flows from the SDK — including versions, runs, webhooks, scripts, and quotas.
#Automation client
sdk.automate gives you full programmatic access to the automation engine. Every method wraps a REST endpoint under /api/v1/automate/. All methods throw ApiError on failure.
#Flow lifecycle
Flows are created as drafts. A draft never runs in production — you publish it to create an immutable version, then activate the flow:
import { EmuView } from '@emuview/sdk';
const sdk = new EmuView({
url: 'https://your-api.example.com',
auth: { apiKey: 'sk-your-api-key' }
});
// 1. Create a flow (always starts in draft)
const { data: flow } = await sdk.automate.createFlow({
name: 'Welcome Email',
trigger_type: 'event_action',
trigger_config: { events: ['record.create'] },
collections: ['users'],
operations: [
{
key: 'send_welcome',
type: 'send_email',
config: {
to: '{{$trigger.body.email}}',
subject: 'Welcome!',
body: 'Hello {{$trigger.body.name}}, welcome aboard!'
}
}
]
});
// 2. Dry-run the draft with mock data
const { data: test } = await sdk.automate.testFlow(flow.id, {
email: 'alex@example.com',
name: 'Alex Chen'
});
console.log(test.status, test.steps.length);
// 3. Publish and activate
await sdk.automate.publishFlow(flow.id, 'Initial version');
await sdk.automate.setFlowStatus(flow.id, 'active');
| Status | Meaning |
|---|---|
draft |
Editable. Test with testFlow(). Triggers don't fire |
active |
Live — the published version runs when the trigger fires |
inactive |
Paused. Triggers are ignored until reactivated |
Trigger types: event_action, event_filter, webhook, schedule, manual, api_endpoint, another_flow, app_event.
#Flow CRUD
| Method | HTTP | Returns |
|---|---|---|
listFlows() |
GET /automate/flows |
{ data: FlowDefinition[] } |
createFlow(input) |
POST /automate/flows |
{ data: FlowDefinition } |
getFlow(flowId) |
GET /automate/flows/:id |
{ data: FlowDetail } |
updateFlow(flowId, input) |
PATCH /automate/flows/:id |
{ data: FlowDefinition } |
deleteFlow(flowId) |
DELETE /automate/flows/:id |
{ success: boolean } |
setFlowStatus(flowId, status) |
PATCH /automate/flows/:id/status |
{ success, status } |
duplicateFlow(flowId, name?) |
POST /automate/flows/:id/duplicate |
{ data: FlowDefinition } |
#createFlow(input) parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Flow name (1–200 characters) |
slug |
string |
No | Identity within the project, ^[a-z0-9][a-z0-9_-]{0,199}$. Derived from the name when omitted; a taken one is a 409 already_exists |
trigger_type |
TriggerType |
Yes | One of the trigger types above |
trigger_config |
object |
No | Trigger-specific settings (e.g. { events: ['record.create'] }, { cron: '0 9 * * *' }) |
operations |
OperationNode[] |
No | Ordered steps. Each has key, type, config, and optional condition, onError, retryConfig, fallbackValue |
collections |
string[] |
No | Collections in scope for CRUD-event triggers |
description, icon, color |
string |
No | Display metadata |
options |
FlowOptions |
No | timeout_ms, max_retries, concurrency, error_handling, log_level |
updateFlow() accepts the same fields, all optional, plus run_as — but not slug. A flow's slug is its identity: it is set once, on create, and is what an export document, a blueprint cell and the Install Ledger refer to the flow by, so it is not routinely renamed.
Every flow carries slug, unique within its project, and it travels through read, export and import. Two projects on the same instance may hold the same slug — it identifies a flow within a project and says nothing about any other. Importing an exported document into the project it came from therefore answers 409 already_exists unless you address the original with ?flowId= (an update) or give the copy a slug of its own.
getFlow() additionally returns publishedOperations (the live snapshot) and webhookEndpoint ({ token } for webhook flows).
#Versioning
| Method | HTTP | Returns |
|---|---|---|
publishFlow(flowId, changeSummary?) |
POST /automate/flows/:id/publish |
{ data: { version, versionId, publishedAt } } |
listVersions(flowId) |
GET /automate/flows/:id/versions |
{ data: FlowVersion[] } |
getVersion(flowId, version) |
GET /automate/flows/:id/versions/:ver |
{ data: FlowVersion } |
rollbackFlow(flowId, version) |
POST /automate/flows/:id/rollback/:ver |
{ success, restoredVersion } |
Each publish creates an immutable snapshot. Only published versions execute in production, so you can keep editing the draft without downtime. rollbackFlow() requires the manage permission — it overwrites the draft with the older snapshot and republishes it.
const result = await sdk.automate.publishFlow('flow_abc', 'Added retry logic');
console.log(`Published version ${result.data.version}`);
await sdk.automate.rollbackFlow('flow_abc', 2);
#Execution and runs
| Method | HTTP | Returns |
|---|---|---|
executeFlow(flowId, data?) |
POST /automate/flows/:id/execute |
{ data: { runId, flowId, status, startedAt } } |
testFlow(flowId, data?) |
POST /automate/flows/:id/test |
{ data: { runId, status, dryRun, steps, logs } } |
listRuns(options?) |
GET /automate/runs |
PaginatedResponse<FlowRun> |
listFlowRuns(flowId, options?) |
GET /automate/flows/:id/runs |
PaginatedResponse<FlowRun> |
getRun(runId) |
GET /automate/runs/:id |
{ data: FlowRunDetail } |
deleteRun(runId) |
DELETE /automate/runs/:id |
{ success: boolean } |
cancelRun(runId) |
POST /automate/runs/:id/cancel |
{ success: boolean } |
cleanupStaleRuns() |
POST /automate/runs/cleanup-stale |
{ success, reaped } |
executeFlow() is asynchronous: it returns a runId immediately, and the flow runs in the background. The flow must be active. Poll getRun() for the outcome:
const { data: result } = await sdk.automate.executeFlow('flow_abc', {
customer_id: 'cust_01HXK5M'
});
const { data: run } = await sdk.automate.getRun(result.runId);
console.log(`Status: ${run.status}, duration: ${run.duration_ms}ms`);
// Step-by-step logs for debugging
for (const step of run.stepLogs) {
console.log(`${step.step_key} (${step.operation_type}): ${step.status}`);
if (step.error) console.error(` Error: ${step.error}`);
}
testFlow() is synchronous: it executes the draft operations with your mock trigger data and returns the full step trace. Test runs are not persisted to the runs table.
listRuns() accepts { page?, limit?, status?, flow_id? } where status is one of pending, running, completed, failed, cancelled, timed_out.
cancelRun() only works on runs still in pending or running state. cleanupStaleRuns() marks any run active longer than 2 minutes as failed — the server also does this automatically before each execution and hourly.
#Webhooks
Webhook flows expose a public URL that external services can call without authentication. triggerWebhook() is a convenience wrapper that sends an unauthenticated request:
// Create and activate a webhook flow
const { data: flow } = await sdk.automate.createFlow({
name: 'Payment Webhook',
trigger_type: 'webhook',
trigger_config: { allowed_methods: ['POST'] },
operations: [
{
key: 'save',
type: 'crud_create',
config: {
collection: 'payments',
data: { amount: '{{$trigger.body.amount}}' }
}
}
]
});
await sdk.automate.publishFlow(flow.id);
await sdk.automate.setFlowStatus(flow.id, 'active');
// The token lives on the flow detail
const { data: detail } = await sdk.automate.getFlow(flow.id);
const token = detail.webhookEndpoint!.token;
// Trigger it — no auth required
const result = await sdk.automate.triggerWebhook(token, {
event: 'payment.completed',
amount: 99.99
});
console.log(`Run started: ${result.runId}`);
| Method | Signature | Returns |
|---|---|---|
triggerWebhook(token, data?, method?) |
method is 'POST' (default) or 'GET' |
{ success, runId, message } |
callEndpoint(flowId, data?, method?) |
For api_endpoint flows; authenticated. method defaults to 'POST' |
{ success, runId, message } |
#Import and export
| Method | Type | Returns |
|---|---|---|
exportFlow(flow) |
Client-side (no network request) | FlowExportDocument |
importFlow(document) |
POST /automate/flows/import |
{ data: FlowDefinition } |
Flows serialise to the portable emuview-flow-v1 JSON format, so you can back them up or move them between projects:
const { data: flow } = await sdk.automate.getFlow('flow_abc');
const exported = sdk.automate.exportFlow(flow);
// Round-trip into another project (imported flows start as drafts)
const { data: copy } = await otherSdk.automate.importFlow(exported);
#Scripts
Reusable JavaScript blocks, referenced by scriptId in run_script operations. Scripts run in a sandbox with resource limits.
| Method | HTTP | Returns |
|---|---|---|
listScripts() |
GET /automate/scripts |
{ data: ScriptDefinition[] } |
createScript(input) |
POST /automate/scripts |
{ data: ScriptDefinition } |
updateScript(scriptId, input) |
PATCH /automate/scripts/:id |
{ data: ScriptDefinition } |
deleteScript(scriptId) |
DELETE /automate/scripts/:id |
{ success: boolean } |
testScript(scriptId, data?) |
POST /automate/scripts/:id/test |
{ data: { output, stats, logs } } |
createScript() accepts { name, code, description?, language?, max_cpu_ms?, max_memory_mb?, allowed_hosts? }.
const { data: script } = await sdk.automate.createScript({
name: 'Slug Generator',
code: `return { slug: $data.title.toLowerCase().replace(/\\s+/g, '-') };`
});
const { data: result } = await sdk.automate.testScript(script.id, {
title: 'Hello World'
});
console.log(result.output); // { slug: 'hello-world' }
console.log(result.stats.cpuTimeMs); // Execution stats
#Expressions
Test a JSONata expression against mock context data without creating a flow:
const result = await sdk.automate.testExpression('$trigger.body.items[status = "active"].name', {
trigger: {
body: {
items: [
{ name: 'Widget', status: 'active' },
{ name: 'Gadget', status: 'draft' }
]
}
}
});
console.log(result.data.output); // ["Widget"]
testExpression(expression, context?) returns { data: { output, durationMs, expressionType } }.
#Operations catalog
| Method | HTTP | Returns |
|---|---|---|
listOperations() |
GET /automate/operations |
{ data: OperationInfo[] } |
getOperation(type) |
GET /automate/operations/:type |
{ data: OperationDetail } |
Each OperationInfo describes one operation type: type, name, description, category, icon, formFields (for building config UIs), and an optional outputShape. getOperation() adds the full configSchema and outputSchema.
const { data: ops } = await sdk.automate.listOperations();
const aiOps = ops.filter((op) => op.category === 'ai');
#Email, quotas, and settings
| Method | HTTP | Returns |
|---|---|---|
getEmailLog(options?) |
GET /automate/email/log |
PaginatedResponse<EmailLogEntry> |
getEmailStats() |
GET /automate/email/stats |
{ data: EmailStats } |
getQuotas() |
GET /automate/quotas |
{ data: ProjectQuotas } |
updateQuotas(input) |
PATCH /automate/quotas |
{ data: ProjectQuotas } |
getAISettings() |
GET /automate/settings/ai |
AISettings |
updateAISettings(settings) |
PUT /automate/settings/ai |
{ success: boolean } |
getEmailSettings() |
GET /automate/settings/email |
EmailSettings |
updateEmailSettings(settings) |
PUT /automate/settings/email |
{ success: boolean } |
const { data: quotas } = await sdk.automate.getQuotas();
console.log(`Flows: ${quotas.currentFlowCount}/${quotas.maxFlows}`);
console.log(`Runs this hour: ${quotas.runsThisHour}/${quotas.maxRunsPerHour}`);
// Requires 'manage' permission
await sdk.automate.updateQuotas({ maxFlows: 100, maxRunsPerHour: 1000 });
await sdk.automate.updateAISettings({
provider: 'cloudflare',
model: '@cf/meta/llama-3.1-8b-instruct'
});
await sdk.automate.updateEmailSettings({
from_address: 'noreply@your-app.example.com',
daily_limit: 1000
});
#Errors
| Status | Code | Cause |
|---|---|---|
| 401 | unauthorized |
Missing or invalid auth token |
| 403 | forbidden |
Insufficient permission (e.g. rollbackFlow or updateQuotas without manage) |
| 404 | not_found |
Flow, run, or script does not exist |
| 400 | limit_exceeded |
Flow count or run-rate quota reached |
| 422 | validation_error |
Invalid flow, operation, or script configuration |
See error handling for catching ApiError and the error reference for the full catalogue.