| name | deterministic-automations |
| description | Create and maintain durable deterministic automations (known as workflows) for the current workspace using Cloudflare Dynamic Workflows. Use when the user asks for scheduled code that should run without another model turn, durable sleeps/retries, workflow steps, or automation scripts. |
| license | Complete terms in LICENSE.txt |
Workflows
Workflows are workspace-scoped Cloudflare Dynamic Workflow scripts. They run deterministic JavaScript code on a UTC cron schedule without sending a prompt to a chat thread.
Use agent tasks when the user wants the agent to think or write a reply later. Use workflows when the user wants predictable code execution, durable workflow steps, retries, sleeps, or event waits.
Tools
list_workflows - list workspace workflows and their virtual source paths.
validate_workflow - check source syntax and required exports before saving.
create_workflow - create a workflow with { name, source, cron_expression, description, enabled? }. Description is required and should summarize what the workflow does.
update_workflow - update metadata, schedule, enabled state, or source with { workflow_id, ... }.
delete_workflow - delete the schedule. Already-started workflow instances may still need their versioned source.
run_workflow_now - start the workflow immediately. Runs are asynchronous and return an instance_id; poll get_workflow_run to see when they finish — don't block-wait.
get_workflow_run - inspect a workflow's recent runs with { workflow_id, limit? }. Returns { latest, runs: [{ instance_id, status, trigger, started_at, completed_at, duration_ms, error }] }. status is started (still running), success, or error (message in error). Sample recent duration_ms for a sensible wait, then poll until the latest run is no longer started. (Per-step status isn't available from the Workflows binding yet.)
Workflow scripts are also exposed as virtual files:
/workspace/.camelai/automations/<workflow_id>.js
After creating a workflow, use read, edit, or write on that path to inspect or update the script like a normal file. New workflows must be created with create_workflow because the schedule and display name are metadata, not file contents.
Script Shape
Every script must export AutomationWorkflow:
import { WorkflowEntrypoint } from "cloudflare:workers";
export class AutomationWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const payload = event.payload;
await step.do("record run", async () => {
console.log("Workflow fired", payload);
});
return { ok: true, firedAt: payload.triggeredAt };
}
}
Imports: a workflow runs as a single module with only the injected bindings.
The only import you may use is import { WorkflowEntrypoint } from "cloudflare:workers".
There is no npm, no URL/CDN imports (e.g. esm.sh), and no relative/multi-file
modules — they fail at runtime. Use the bindings below (env.TOOLS,
env.CONNECTIONS, env.AI) and built-in Web APIs (fetch, crypto, …) for
everything else.
The workflow receives event.payload with:
{
workspaceId,
workflowId,
workflowName,
scheduledFor,
triggeredAt,
trigger
}
Legacy aliases automationId and automationName are also present in the payload.
Available Bindings
Inside AutomationWorkflow, use this.env:
this.env.CONNECTIONS - workspace connection binding.
this.env.AI - virtual AI binding when deterministic code needs a model call.
this.env.TOOLS - harness tool binding for non-interactive platform tools.
Prefer deterministic connection calls over agent-like orchestration:
import { WorkflowEntrypoint } from "cloudflare:workers";
export class AutomationWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const result = await step.do("query database", async () => {
const db = await this.env.CONNECTIONS.find("postgres");
const connections = this.env.CONNECTIONS;
return await connections[db.alias].query({
query: "SELECT count(*) AS total FROM users",
});
});
return { users: result };
}
}
Durable Workflow Patterns
Use step.do for side effects and retryable units:
await step.do("sync external API", { retries: { limit: 3, delay: "30 seconds" } }, async () => {
const response = await fetch("https://api.example.com/sync", { method: "POST" });
if (!response.ok) throw new Error(`Sync failed: ${response.status}`);
return await response.json();
});
Use step.sleep for long waits that should survive Worker restarts:
await step.sleep("wait for settlement", "2 hours");
await step.do("check status", async () => {
});
Use step.waitForEvent only when the app has a clear way to send the event to the workflow instance:
const approval = await step.waitForEvent("wait for approval", {
type: "approval.received",
timeout: "7 days",
});
Create Workflow
- Write the source.
- Call
validate_workflow with the source.
- Call
create_workflow.
- Call
run_workflow_now for a smoke test when the action is safe.
Example:
const source = `import { WorkflowEntrypoint } from "cloudflare:workers";
export class AutomationWorkflow extends WorkflowEntrypoint {
async run(event, step) {
return await step.do("daily health check", async () => {
const response = await fetch("https://example.com/health");
return { ok: response.ok, status: response.status, scheduledFor: event.payload.scheduledFor };
});
}
}
`;
await tools.validate_workflow({ source });
await tools.create_workflow({
name: "Daily health check",
description: "Fetches the public health endpoint every morning.",
source,
cron_expression: "0 14 * * *",
});
Cron expressions are 5 fields in UTC: minute hour day-of-month month day-of-week.
Editing
await tools.list_workflows({});
await tools.read({ path: "/workspace/.camelai/automations/<workflow_id>.js" });
await tools.edit({
path: "/workspace/.camelai/automations/<workflow_id>.js",
edits: [{ oldText: "old code", newText: "new code" }],
});
Each source edit creates a new source version. Started workflow instances run against the version they were created with.