Skip to main content

Workflows

Workflows are a sequence of steps that gets executed durably. A workflow can be seen as a special type of Virtual Object with some special characteristics:

  • Each workflow definition has a run handler that implements the workflow logic.
  • The run handler executes exactly one time for each workflow instance (object / key).
  • A workflow definition can implement other handlers that can be called multiple times, and can interact with the workflow.
  • Workflows have access to the WorkflowContext and WorkflowSharedContext, giving them some extra functionality, for example Durable Promises to signal workflows.
Workflow retention time

The retention time of a workflow execution is 24 hours after the finishing of the run handler. After this timeout any K/V state is cleared, the workflow's shared handlers cannot be called anymore, and the Durable Promises are discarded. The retention time can be configured via the Admin API per Workflow definition by setting workflow_completion_retention.

Implementing workflows

Have a look at the code example to get a better understanding of how workflows are implemented:

The run handler

Every workflow needs a run handler. This handler has access to the same SDK features as Service and Virtual Object handlers. For example, use ctx.run to log intermediate results in Restate and avoid re-execution on replay.

signup.ts

import * as restate from "@restatedev/restate-sdk";
import {WorkflowContext} from "@restatedev/restate-sdk";
const signUpWorkflow = restate.workflow({
name: "signup",
handlers: {
run: async (ctx: WorkflowContext, req: { email: string }) => {
const secret = ctx.rand.uuidv4();
ctx.set("status", "Generated secret");
await ctx.run("send email", () =>
sendEmailWithLink({ email: req.email, secret }));
ctx.set("status", "Sent email");
const clickSecret = await ctx.promise<string>("email.clicked");
ctx.set("status", "Clicked email");
return clickSecret == secret;
},
click: (ctx: restate.WorkflowSharedContext, secret: string) =>
ctx.promise<string>("email.clicked").resolve(secret),
getStatus: (ctx: restate.WorkflowSharedContext) =>
ctx.get<string>("status"),
},
});
export type SignUpWorkflow = typeof signUpWorkflow;
restate.endpoint().bind(signUpWorkflow).listen();
// or .lambdaHandler();

Querying workflows

Similar to Virtual Objects, you can retrieve the K/V state of workflows via the other handlers defined in the workflow definition, For example, here we expose the status of the workflow to external clients. Every workflow execution can be seen as a new object, so the state is isolated to a single workflow execution. The state can only be mutated by the run handler of the workflow. The other handlers can only read the state.

signup.ts

import * as restate from "@restatedev/restate-sdk";
import {WorkflowContext} from "@restatedev/restate-sdk";
const signUpWorkflow = restate.workflow({
name: "signup",
handlers: {
run: async (ctx: WorkflowContext, req: { email: string }) => {
const secret = ctx.rand.uuidv4();
ctx.set("status", "Generated secret");
await ctx.run("send email", () =>
sendEmailWithLink({ email: req.email, secret }));
ctx.set("status", "Sent email");
const clickSecret = await ctx.promise<string>("email.clicked");
ctx.set("status", "Clicked email");
return clickSecret == secret;
},
click: (ctx: restate.WorkflowSharedContext, secret: string) =>
ctx.promise<string>("email.clicked").resolve(secret),
getStatus: (ctx: restate.WorkflowSharedContext) =>
ctx.get<string>("status"),
},
});
export type SignUpWorkflow = typeof signUpWorkflow;
restate.endpoint().bind(signUpWorkflow).listen();
// or .lambdaHandler();

Signaling workflows

You can use Durable Promises to interact with your running workflows: to let the workflow block until an event occurs, or to send a signal / information into or out of a running workflow. These promises are durable and distributed, meaning they survive crashes and can be resolved or rejected by any handler in the workflow.

Do the following:

  1. Create a promise in your one handler that is durable and distributed. For example, here in the run handler.
  2. Resolve or reject the promise in any other handler in the workflow. This can be done at most one time.
signup.ts

import * as restate from "@restatedev/restate-sdk";
import {WorkflowContext} from "@restatedev/restate-sdk";
const signUpWorkflow = restate.workflow({
name: "signup",
handlers: {
run: async (ctx: WorkflowContext, req: { email: string }) => {
const secret = ctx.rand.uuidv4();
ctx.set("status", "Generated secret");
await ctx.run("send email", () =>
sendEmailWithLink({ email: req.email, secret }));
ctx.set("status", "Sent email");
const clickSecret = await ctx.promise<string>("email.clicked");
ctx.set("status", "Clicked email");
return clickSecret == secret;
},
click: (ctx: restate.WorkflowSharedContext, secret: string) =>
ctx.promise<string>("email.clicked").resolve(secret),
getStatus: (ctx: restate.WorkflowSharedContext) =>
ctx.get<string>("status"),
},
});
export type SignUpWorkflow = typeof signUpWorkflow;
restate.endpoint().bind(signUpWorkflow).listen();
// or .lambdaHandler();

Serving and registering workflows

You serve workflows in the same way as Services and Virtual Objects: by binding them to an HTTP endpoint or AWS Lambda handler. Make sure you register the endpoint or Lambda handler in Restate before invoking it.

signup.ts

import * as restate from "@restatedev/restate-sdk";
import {WorkflowContext} from "@restatedev/restate-sdk";
const signUpWorkflow = restate.workflow({
name: "signup",
handlers: {
run: async (ctx: WorkflowContext, req: { email: string }) => {
const secret = ctx.rand.uuidv4();
ctx.set("status", "Generated secret");
await ctx.run("send email", () =>
sendEmailWithLink({ email: req.email, secret }));
ctx.set("status", "Sent email");
const clickSecret = await ctx.promise<string>("email.clicked");
ctx.set("status", "Clicked email");
return clickSecret == secret;
},
click: (ctx: restate.WorkflowSharedContext, secret: string) =>
ctx.promise<string>("email.clicked").resolve(secret),
getStatus: (ctx: restate.WorkflowSharedContext) =>
ctx.get<string>("status"),
},
});
export type SignUpWorkflow = typeof signUpWorkflow;
restate.endpoint().bind(signUpWorkflow).listen();
// or .lambdaHandler();

The run handler

Every workflow needs a run handler. This handler has access to the same SDK features as Service and Virtual Object handlers. For example, use ctx.run to log intermediate results in Restate and avoid re-execution on replay.

Querying workflows

Similar to Virtual Objects, you can retrieve the K/V state of workflows via the other handlers defined in the workflow definition, For example, here we expose the status of the workflow to external clients. Every workflow execution can be seen as a new object, so the state is isolated to a single workflow execution. The state can only be mutated by the run handler of the workflow. The other handlers can only read the state.

Signaling workflows

You can use Durable Promises to interact with your running workflows: to let the workflow block until an event occurs, or to send a signal / information into or out of a running workflow. These promises are durable and distributed, meaning they survive crashes and can be resolved or rejected by any handler in the workflow.

Do the following:

  1. Create a promise in your one handler that is durable and distributed. For example, here in the run handler.
  2. Resolve or reject the promise in any other handler in the workflow. This can be done at most one time.

Serving and registering workflows

You serve workflows in the same way as Services and Virtual Objects: by binding them to an HTTP endpoint or AWS Lambda handler. Make sure you register the endpoint or Lambda handler in Restate before invoking it.

signup.ts

import * as restate from "@restatedev/restate-sdk";
import {WorkflowContext} from "@restatedev/restate-sdk";
const signUpWorkflow = restate.workflow({
name: "signup",
handlers: {
run: async (ctx: WorkflowContext, req: { email: string }) => {
const secret = ctx.rand.uuidv4();
ctx.set("status", "Generated secret");
await ctx.run("send email", () =>
sendEmailWithLink({ email: req.email, secret }));
ctx.set("status", "Sent email");
const clickSecret = await ctx.promise<string>("email.clicked");
ctx.set("status", "Clicked email");
return clickSecret == secret;
},
click: (ctx: restate.WorkflowSharedContext, secret: string) =>
ctx.promise<string>("email.clicked").resolve(secret),
getStatus: (ctx: restate.WorkflowSharedContext) =>
ctx.get<string>("status"),
},
});
export type SignUpWorkflow = typeof signUpWorkflow;
restate.endpoint().bind(signUpWorkflow).listen();
// or .lambdaHandler();

Submitting workflows with SDK clients

Submit: This returns a handle to the workflow once has been registered in Restate. You can only submit once per workflow ID (here "someone").


// import * as restate from "@restatedev/restate-sdk-clients";
const rs = restate.connect({url: "http://localhost:8080"});
const handle = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.workflowSubmit({email: user.email});

Submit: This returns a handle to the workflow once has been registered in Restate. You can only submit once per workflow ID (here "someone").


// import * as restate from "@restatedev/restate-sdk-clients";
const rs = restate.connect({url: "http://localhost:8080"});
const handle = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.workflowSubmit({email: user.email});

Query/signal: Call the other handlers of the workflow in the same way as for Virtual Object handlers. For now, you can only do request-response calls.


const status = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.getStatus();

Query/signal: Call the other handlers of the workflow in the same way as for Virtual Object handlers. For now, you can only do request-response calls.


const status = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.getStatus();

Attach/peek: This lets you retrieve the result of a workflow or check if it's finished.


// Option 1: attach and wait for result with handle
const result1 = await rs.result(handle);
// Option 2: attach and wait for result with workflow ID
const result2 = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.workflowAttach();
// Option 3: peek to check if ready with workflow ID
const peekOutput = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.workflowOutput();
if(peekOutput.ready){
const result3 = peekOutput.result;
}

Attach/peek: This lets you retrieve the result of a workflow or check if it's finished.


// Option 1: attach and wait for result with handle
const result1 = await rs.result(handle);
// Option 2: attach and wait for result with workflow ID
const result2 = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.workflowAttach();
// Option 3: peek to check if ready with workflow ID
const peekOutput = await rs
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.workflowOutput();
if(peekOutput.ready){
const result3 = peekOutput.result;
}

Submitting workflows from a Restate service

Submit/query/signal: Call the workflow handlers in the same way as for Services and Virtual Objects. This returns the result of the workflow/handler once it has finished. Use the workflowSendClient to call the handler without waiting for the result. You can only call the run handler (submit) once per workflow ID (here "someone").


signUpUser: async (ctx: ObjectContext, email: string) => {
const result = await ctx
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.run({email});
},
queryStatus: async (ctx: ObjectContext) => {
const status = await ctx
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.getStatus();
},

Submit/query/signal: Call the workflow handlers in the same way as for Services and Virtual Objects. This returns the result of the workflow/handler once it has finished. Use the workflowSendClient to call the handler without waiting for the result. You can only call the run handler (submit) once per workflow ID (here "someone").


signUpUser: async (ctx: ObjectContext, email: string) => {
const result = await ctx
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.run({email});
},
queryStatus: async (ctx: ObjectContext) => {
const status = await ctx
.workflowClient<SignUpWorkflow>({name: "signup"}, "someone")
.getStatus();
},

Submitting workflows over HTTP

Submit/query/signal: Call any handler of the workflow in the same way as for Services and Virtual Objects. This returns the result of the handler once it has finished. Add /send to the path for one-way calls. You can only call the run handler once per workflow ID (here "someone").


curl localhost:8080/signup/someone/run \
-H 'content-type: application/json' \
-d '{"email": "[email protected]"}'

Submit/query/signal: Call any handler of the workflow in the same way as for Services and Virtual Objects. This returns the result of the handler once it has finished. Add /send to the path for one-way calls. You can only call the run handler once per workflow ID (here "someone").


curl localhost:8080/signup/someone/run \
-H 'content-type: application/json' \
-d '{"email": "[email protected]"}'

Attach/peek: This lets you retrieve the result of a workflow or check if it's finished.


curl localhost:8080/restate/workflow/signup/someone/attach
curl localhost:8080/restate/workflow/signup/someone/output

Attach/peek: This lets you retrieve the result of a workflow or check if it's finished.


curl localhost:8080/restate/workflow/signup/someone/attach
curl localhost:8080/restate/workflow/signup/someone/output

Inspecting workflows

Have a look at the introspection docs on how to inspect workflows. You can use this to for example: