Language Syntax
Rhythm workflows are written in .flow files using a custom scripting language designed for durable execution. The syntax is a strictly defined, sandboxed subset of JavaScript.
Unlike traditional JavaScript, every await point in Rhythm acts as a persistence boundary. When the interpreter reaches an await, the entire runtime state (including local variables and the call stack) is serialized to the database. This allows the workflow to resume exactly where it left off, even across process restarts, without replaying previous events.
Basic Syntax
Rhythm supports standard JavaScript expressions and variable declarations.
Variables and Data Types
Variables are declared using the let keyword. The engine supports standard JSON-compatible types:
- Numbers:
let count = 1.5 - Strings:
let name = "workflow_id" - Booleans:
let isActive = true - Objects:
let data = { key: "value" } - Arrays:
let list = [1, 2, 3] - Null:
let result = null
Operators
Rhythm supports standard arithmetic (+, -, *, /) and logical operators (&&, ||, !).
Control Flow
Workflows support common logic structures to orchestrate complex tasks.
Loops
The for...of loop is the primary way to iterate over collections. You can use await inside loops to perform sequential durable tasks.
for (let item of Inputs.items) {
await Task.run("process-item", { id: item.id });
}
Error Handling
Reliable error handling is achieved via try/catch blocks. If an awaited task fails, it throws an error that can be caught within the workflow logic.
try {
await Task.run("unreliable-task", {});
} catch (err) {
// handle error or run cleanup task
await Task.run("notify-failure", { error: err.message });
}
Global Objects and APIs
The workflow engine provides several global objects specifically for orchestration.
Inputs
The Inputs object contains the parameters passed to the workflow when it was started.
// Accessing a specific input
let userId = Inputs.userId;
Task
The Task object is used to invoke application-level tasks.
Task.run(name: string, args: object): Triggers a task and waits for its completion.- Fire-and-forget: Calling
Task.runwithoutawaitenqueues the task but does not pause the workflow to wait for the result.
// Run and wait
let result = await Task.run("calculate-total", { items: [1, 2] });
// Fire and forget
Task.run("log-event", { status: "started" });
Signal
Signals allow workflows to pause and wait for external events.
Signal.when(name: string, options?: object): Pauses the workflow until a signal with the specified name is received.
// Pause until external approval, with a timeout
try {
let approval = await Signal.when("manager-approval", { timeout: "24h" });
} catch (err) {
// Handle timeout
}
Math
Rhythm provides a subset of the standard JavaScript Math library for basic calculations:
Math.floor(n)Math.ceil(n)Math.round(n)Math.abs(n)
let rounded = Math.round(Inputs.price);
Returns and Outputs
A workflow ends when it reaches a return statement or the end of the script. The returned value is persisted as the workflow's final output and can be read by parent workflows or the calling application.
return {
status: "success",
processedCount: count
};
Immutability and Versioning
Workflow scripts are self-versioning. When a workflow starts, Rhythm calculates a content hash of the .flow file.
- In-progress workflows are locked to the version they started with.
- New workflows will use the latest version of the file.
- This allows you to modify workflow logic and re-deploy without breaking currently running executions.