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.