Workflow documentation
A Workflow is a way to chain Actions. The syntax is similar to how async/await syntax works.
A Workflow is also a specific type of Action
, so you can chain Workflows in other Workflows.

Write a Workflow
A Workflow, same as an Action
, has an init()
method.
A Workflow also has a define()
method where you can write your flow of Actions.
export class MyWorkflow extends Workflow {
define() {
try {
await this.do(
'my-action',
new MyAction().setArgument({
x: 1,
})
);
} catch (err) {
await this.do(
'on-error-action',
new OnErrorAction().setArgument({
x: 1,
})
);
} finally {
//....
}
return {
x: 2,
};
}
}
The do method
Purpose
The do()
method is used to declare an action to be executed within the workflow. It mimics the behavior of await in native JavaScript/TypeScript syntax to provide readable, sequential flow control.
However, do()
is more complex than a normal asynchronous call. Behind the scenes, the orbits framework implements deterministic execution with built-in replayability. This means:
define()
can be executed multiple times during the lifecycle of a workflow.- Each call to
do()
is idempotent: the action is only executed once. - On re-execution, previously completed
do()
calls return the stored result instead of re-running the action.
Point of attention: deterministic execution and async/await
export class BadWorkflow extends Workflow {
define() {
const search = await axios.get('https://google.com?search=test'); // ❌ Not deterministic
if (search.length) {
await this.do('step-a', new StepA());
} else {
await this.do('step-b', new StepB());
}
return { status: 'done' };
}
}
export class CorrectWorkflow extends Workflow {
define() {
const search = await this.do('search', () => {
axios.get('https://google.com?search=test');
});
if (search.length) {
await this.do('step-a', new StepA());
} else {
await this.do('step-b', new StepB());
}
return { status: 'done' };
}
}
export class BadWorkflow extends Workflow {
define() {
const now = Date.now(); // ❌ Not deterministic
if (now % 2 === 0) {
await this.do('step-a', new StepA());
} else {
await this.do('step-b', new StepB());
}
return { now };
}
}
export class CorrectWorkflow extends Workflow {
IBag: {
now: number;
};
async init() {
await super.init();
if (!this.bag.now) {
this.bag.now = Date.now();
}
}
define() {
if (this.bag.now % 2 === 0) {
await this.do('step-a', new StepA());
} else {
await this.do('step-b', new StepB());
}
return { now: this.bag.now };
}
}
Action Step definition
The do()
method accepts an Action
instance as its argument:
await this.do('step-a', new MyAction().setArgument({ some: 'argument' }));
The action will be persisted in the database and executed in an isolated context.
- The result of the
await
will be the result of the action. - If the final action's state is
ActionState.ERROR
, thedo()
method will throw the error — allowing standard error handling using try/catch.
On the fly promise definition
The do()
method also accepts a callback that returns a Promise.
await this.do('step-a', () => {
return Promise.resolve();
});
await this.do('step-b', myAsyncFunction.bind('some', 'argument'));
- The promise will be executed once and only once.
- The result of the await will be the resolved value of the promise.
- If an error occurs, it will be thrown by
do()
and can be handled viatry/catch
.
Dynamic definition
Dynamic definitions are useful when you need to pass non-serializable parameters, such as functions.
Since actions are stored in the database, only JSONObject
-compatible properties can be persisted.
To pass methods or closures through the chain of actions, use the dynamicAction
option:
const action = new MyAction();
// override a method of your action
action.getCredentials = () => this.specificConfig.getCredentials();
await this.do('step-a', { dynamicAction: action });
The action will be persisted in the database and executed in an isolated context.
When executed, getCredentials
will be overridden with this definition specific to the workflow context.
- The result of the
await
will be the result of the action. - If the final action's state is
ActionState.ERROR
, thedo()
method will throw the error — allowing standard error handling using try/catch.
Under the hood
Under the hood, the do
method returns a standard JavaScript promise.
However this promise does not always resolve.
Let's say we have a workflow with two steps:
export class MyWorkflow extends Workflow {
define() {
const actionA = new LongTermAction();
const result = await this.do('step-a', actionA);
await this.do('step-b', new MyAction().setArgument(result));
}
}
Here is the flow of calls that happens.
Repeat and transform
Action level
At the action level, you can configure the repetition using the setRepeat()
method.
Example:
export class MyWorkflow extends Workflow {
define() {
try {
await this.do(
'my-action',
new MyAction()
.setArgument({
x: 1,
})
.setRepeat({
[ActionState.ERROR]: 2, // repeat twice, so will be executed max. 3 times
})
);
} catch (err) {
await this.do(
'on-error-action',
new OnErrorAction().setArgument({
x: 1,
})
);
} finally {
//....
}
return {
x: 2,
};
}
}
Workflow level
At the workflow level, you can configure the repetition using the repeatDo()
method.
Example:
export class WorkflowWithRepeat extends Workflow {
async define() {
await this.repeatDo(
'repeatSuccess',
() => {
//...do something...
return Promise.resolve();
},
{
[ActionState.SUCCESS]: 2,
elapsedTime: 10,
}
);
try {
await this.repeatDo(
'repeatOnFailure',
() => {
//...do something...
return Promise.reject();
},
{
[ActionState.ERROR]: 2,
elapsedTime: 10,
}
);
} catch (err) {}
return i;
}
}
Transformation
The transform()
method is a powerful hook that intercepts every Action before it is executed by the Workflow.
It acts as a proxy layer over all Actions that pass through the Workflow engine. You can use it to:
- Modify the arguments of any Action;
- Replace one Action with another;
- Apply conditional logic;
- Dynamically set
.setRepeat()
,.setTimeout()
,.setBag()
, etc.
This is especially useful when you want to customize or override an Action behavior without changing the Workflow's define()
logic.
Examples
Simple example
export class MyWorkflow extends Workflow {
// The transform method allows you to intercept and modify actions dynamically.
transform(ref: string, action: Action): Action {
if (ref === 'my-action') {
// Override the arguments for the step "my-action"
return MyAction().setArgument({
x: 2,
});
}
// Returning `undefined` means the original action instance will be used as-is
return;
}
async define() {
try {
await this.do(
'my-action',
MyAction()
.setArgument({
x: 1,
})
.setRepeat({
[ActionState.ERROR]: 2, // Retry up to 2 times on error (executed max 3 times)
})
);
} catch (err) {
await this.do(
'on-error-action',
OnErrorAction().setArgument({
x: 1,
})
);
} finally {
// You can perform cleanup or additional steps here
}
return {
x: 2,
};
}
}
Key Notes
- The
ref
argument passed totransform()
matches the name you use inthis.do("my-action", ...)
. - You must return a valid Action instance if you want to replace or modify it.
- If
transform()
returnsundefined
, the originalAction
provided indefine()
will be used.
Use this to:
- Inject environment-specific parameters (e.g. test vs. prod);
- Dynamically change retry logic or timeouts;
- Centralize logic for versioning or conditional behavior.
Inheritance example
export class CustomWorkflow extends BaseWorkflow {
transform(ref: string, action: Action): Action {
if (ref === 'notify') {
// Override the default email step with a custom SMS notification
return new SendSmsAction().setArgument({
message: 'This replaces the email notification',
});
}
return;
}
}
This pattern allows you to:
- Reuse shared workflow logic;
- Swap or extend individual steps based on context;
- Build a hierarchy of workflows with consistent interfaces but customizable behavior.