# Check the registered deployments
restate deployments list
```
Some deployment targets support only HTTP/1.1. Use `--use-http1.1` when registering these deployments.
For AWS Lambda, use the function ARN instead of a URL. See [AWS Lambda deployment](/services/deploy/lambda).
Avoid long-running handlers (days or months) - otherwise you need to keep old deployments around until all invocations complete. Instead, break work into smaller chunks using delayed calls.
## Automatic versioning with FaaS platforms
Function-as-a-Service (FaaS) platforms automatically handle immutable versioning through version-specific URLs or ARNs.
This makes them ideal for Restate deployments as they eliminate the complexity of manual version management.
Have a look at the dedicated deployment docs to learn more:
* [Vercel](/services/deploy/vercel#register-the-service-to-restate): Register the Commit URL so that Restate can address specific Vercel deployments.
* [AWS Lambda](/services/deploy/lambda): When you [publish a Lambda function](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html), it automatically creates an immutable version with a unique ARN that never changes.
* [Deno Deploy](/services/deploy/deno-deploy#register-the-service-to-restate): Register the Preview URLs so that Restate can target specific Deno deployments.
* [Cloudflare Workers](/services/deploy/cloudflare-workers#register-the-service-to-restate): Register the Preview URLs so that Restate can target specific Cloudflare deployments.
## Automatic versioning with Kubernetes Operator
The [Restate Kubernetes operator](/services/deploy/kubernetes#deployment-with-restate-operator) provides a higher-level abstraction for managing service deployments and their versions automatically.
The operator handles the complete versioning lifecycle:
1. **Deploy new versions**: Create a new `RestateDeployment` with your updated container image
2. **Automatic registration**: The operator registers the new deployment with the Restate cluster
3. **Traffic routing**: New requests automatically route to the latest version
4. **Graceful draining**: The operator monitors older deployments for ongoing invocations
5. **Auto-scaling to zero**: Once drained, older versions automatically scale to zero
For complete examples and specifications, see [Restate Operator git repo](https://github.com/restatedev/restate-operator).
## Manual versioning
For non-FaaS deployments, you can use either the UI or the CLI to manage deployments. The typical update flow looks like this:
Deploy your updated service code to a new endpoint (e.g., `http://greeter-v2/`).
Then register it with Restate:
```shell CLI theme={null}
restate deployments register http://greeter-v2/
```
Restate automatically routes new requests to the latest deployment. Existing requests continue on the original deployment.
Check for in-flight invocations on deployments [via the UI](https://restate.dev/blog/announcing-restate-ui/#an-aid-for-versioning) or CLI.
```shell theme={null}
# Get detailed information about a specific deployment
# including the list of active invocations
restate deployment describe --extra
```
Once all invocations are complete, you can safely remove the old deployment via the UI or CLI.
```shell theme={null}
restate deployments remove
```
If you need to force removal before the deployment is fully drained, use the `--force` flag in CLI.
Virtual Object state persists across versions. Ensure your state schema remains backward compatible.
## Local development
During local development, you're iterating quickly on your code and don't need immutable deployments.
You can safely re-register the same endpoint after code changes using the `--force` flag:
```shell theme={null}
restate deployments register --force localhost:9080
```
This overwrites the existing deployment registration, allowing Restate to discover your updated service definition.
In-flight invocations might keep failing with [non-determinism errors](/references/errors#rt0016), but this is typically fine during development.
You can kill all the in-flight invocations to a service using either CLI or UI:
```shell CLI theme={null}
restate invocations kill
```
## Advanced operations and troubleshooting
This section covers common scenarios you may encounter when managing deployments, and some of the troubleshooting best practices we recommend.
### Journal mismatch errors
Restate performs journal compatibility checks during replay to prevent corruption.
When you see a journal mismatch error ([RT0016](/references/errors#rt0016)), it means the code executed during replay has produced a different journal than the original execution.
This is typically caused by two scenarios:
**Non-deterministic code**: Code that produces different results on each execution, even with the same inputs.
Examples of non-deterministic operations:
* External operations such as HTTP requests
* Performing any Restate Context operation, such as calling another service using `ctx.serviceClient`, inside `ctx.run` closures
* Random value generation: `Math.random()`, `uuid.v4()`, etc.
* Getting the current time or date: `new Date()`
* Iterating over unordered collections, such as hash maps
* Mutating outer scope variables (e.g. `myVar = newValue`) or object fields (e.g. `myObject.setValue()`) inside `ctx.run`, then relying on those values in the rest of the handler code.
To record non-deterministic operation results for replay, you need to record its results using `ctx.run`.
For more info, see the **durable steps** documentation: [TypeScript](/develop/ts/durable-steps), [Python](/develop/python/durable-steps), [Java/Kotlin](/develop/java/durable-steps), [Go](/develop/go/durable-steps).
**In-place code changes**: Modifying deployed code at the same endpoint. This violates the immutable deployment principle and can cause in-flight invocations to fail when they replay with the updated code.
Unsafe changes include:
* Reordering Restate SDK operations (`run`, state access, service calls, awakeables)
* Adding or removing SDK operations in the execution path
* Changing operation inputs (state keys, service call payloads)
* Modifying conditional logic that affects which operations execute
See the sections above for deployment best practices and the sections below for how to fix in-flight invocations.
### Fixing a bug by updating deployed code
If you have a bug in your deployment code, sometimes it is safe to fix it by updating the deployment in-place.
| Change | Safe? |
| ---------------------------------------------------------------------- | ----- |
| Fixing a bug inside `ctx.run` | ✅ |
| Fixing a bug that consistently reproduces (e.g. deserialization error) | ✅ |
| Changing the order of Restate operations, or adding/removing them | ❌ |
| Changing operation inputs (state keys, call payloads, etc.) | ❌ |
This approach doesn't work on FaaS platforms (e.g. Lambda) or with the Kubernetes Operator, since these use immutable deployments by design.
In those cases, use [pause and resume](#pause-invocations-and-resume-on-a-new-deployment) instead.
### Pause invocations and resume on a new deployment
This is the **recommended approach** for fixing bugs affecting in-flight invocations. It works on all platforms, including FaaS and Kubernetes.
When a bug affects in-flight invocations, they remain pinned to the original deployment.
Registering a new deployment with a fix only helps new invocations. To fix existing invocations:
Deploy your fixed code and register it with Restate:
```shell theme={null}
restate deployments register http://greeter-v2/
```
```shell theme={null}
restate invocations pause
```
```shell theme={null}
restate invocations resume --deployment
```
The fix must be compatible with already-executed journal entries so they can replay successfully.
If the business logic differs, the invocation will fail with [non-determinism errors](/references/errors#rt0016).
For more details, see [managing invocations](/services/invocation/managing-invocations#resume).
### Reassigning a deployment endpoint
This is an advanced technique. In most cases, use [pause and resume](#pause-invocations-and-resume-on-a-new-deployment) instead.
You can use the Admin API to change which endpoint a deployment points to.
This is useful when you've deployed a fix to a new URL and want stuck invocations to use it:
```shell theme={null}
curl -X PUT localhost:9070/deployments/dp_14LsPzGz9HBxXIeBoH5wYUh \
--json '{"uri": "http://greeter-patched/"}'
```
The same determinism rules apply: the fix must be compatible with already-executed journal entries.
### Updating a service interface
When you register a new deployment, Restate validates that it doesn't break existing service interfaces: adding handlers is allowed, but removing or renaming them is not.
For example, given you register a new deployment for the already existing service `Greeter`, renaming its handler from `greet` to `sayGreet` is a breaking change, thus registration will fail.
To allow breaking changes, use the `--breaking` flag:
```shell theme={null}
restate deployments register --breaking http://greeter-v2/
```
Before using `--breaking`, make sure all callers have been updated to use the new service interface.
### Removing a service
In Restate to remove a service, you must remove the deployment that contains it.
To do this safely, follow the steps below:
1. Ensure no other handlers or services have business logic that calls the service you're removing.
2. If several services are bundled in the same deployment, you can't remove only one of them. You have to remove the whole deployment.
So make sure that you first deploy the services you want to keep in a separate new deployment.
3. [Make the service private](/services/security#private-services) to avoid accepting new HTTP requests.
4. Check whether the service has pending invocations by filtering the invocations on deployment ID in the [UI](/installation#restate-ui) or via `restate services status`, and wait until the service is drained (i.e. no ongoing invocations).
**When all prerequisites are fulfilled**, you can remove the deployment containing the service via the [UI](/installation#restate-ui) or via CLI:
```shell theme={null}
restate deployments remove dp_14LsPzGz9HBxXIeBoH5wYUh
```
If the deployment isn't drained yet but you still want to remove it, use the `--force` flag in CLI.
# AI Agents
Source: https://docs.restate.dev/tour/agents
# Microservice Orchestration
Source: https://docs.restate.dev/tour/microservice-orchestration
Learn how to orchestrate microservices with durable execution, sagas, and async communication patterns.
Microservice orchestration is about coordinating multiple services to complete complex business workflows. Restate provides powerful primitives for building resilient, observable orchestration patterns.
In this guide, you'll learn how to:
* Build durable, fault-tolerant service orchestrations with automatic failure recovery
* Implement sagas for distributed transactions with resilient compensation
* Use durable timers and external events for complex async patterns
* Implement stateful entities with Virtual Objects
## Getting Started
A Restate application is composed of two main components:
* **Restate Server**: The core engine that manages durable execution and orchestrates services. It acts as a message broker or reverse proxy in front of your services.
* **Your Services**: Your business logic, implemented as service handlers using the Restate SDK to perform durable operations.
A basic subscription service orchestration looks like this:
```ts src/getstarted/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/getstarted/service.ts?collapse_prequel"} theme={null}
export const subscriptionService = restate.service({
name: "SubscriptionService",
handlers: {
add: async (ctx: Context, req: SubscriptionRequest) => {
const paymentId = ctx.rand.uuidv4();
const payRef = await ctx.run("pay", () =>
createRecurringPayment(req.creditCard, paymentId),
);
for (const subscription of req.subscriptions) {
await ctx.run(`add-${subscription}`, () =>
createSubscription(req.userId, subscription, payRef),
);
}
},
},
});
```
```java getstarted/SubscriptionService.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/getstarted/SubscriptionService.java?collapse_prequel"} theme={null}
@Service
public class SubscriptionService {
@Handler
public void add(SubscriptionRequest req) {
var paymentId = Restate.random().nextUUID().toString();
String payRef =
Restate.run("pay", String.class, () -> createRecurringPayment(req.creditCard(), paymentId));
for (String subscription : req.subscriptions()) {
Restate.run(
"add-" + subscription, () -> createSubscription(req.userId(), subscription, payRef));
}
}
}
```
```go getstarted.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/getstarted.go?collapse_prequel"} theme={null}
type SubscriptionService struct{}
func (SubscriptionService) Add(ctx restate.Context, req SubscriptionRequest) error {
paymentId := restate.UUID(ctx).String()
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateRecurringPayment(req.CreditCard, paymentId)
}, restate.WithName("pay"))
if err != nil {
return err
}
for _, subscription := range req.Subscriptions {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateSubscription(req.UserId, subscription, payRef)
}, restate.WithName(fmt.Sprintf("add-%s", subscription)))
if err != nil {
return err
}
}
return nil
}
```
```python app/getstarted/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/getstarted/service.py?collapse_prequel"} theme={null}
subscription_service = restate.Service("SubscriptionService")
@subscription_service.handler()
async def add(ctx: restate.Context, req: SubscriptionRequest) -> None:
payment_id = str(ctx.uuid())
pay_ref = await ctx.run_typed(
"pay",
create_recurring_payment,
credit_card=req.credit_card,
payment_id=payment_id,
)
for subscription in req.subscriptions:
await ctx.run_typed(
f"add-{subscription}",
create_subscription,
user_id=req.user_id,
subscription=subscription,
payment_ref=pay_ref,
)
```
A service has handlers that can be called over HTTP. Each handler receives a `Context` object that provides durable execution primitives. Any action performed with the Context is automatically recorded and can survive failures.
You don't need to run your services in any special way. Restate works with how you already deploy your code, whether that's in Docker, on Kubernetes, or via AWS Lambda.
The endpoint that serves the services of this tour over HTTP is defined in `src/app.ts`.
The endpoint that serves the services of this tour over HTTP is defined in `AppMain.java`.
The endpoint that serves the services of this tour over HTTP is defined in `main.go`.
The endpoint that serves the services of this tour over HTTP is defined in `__main__.py`.
### Run the example
[Install Restate](/installation) and launch it:
```bash theme={null}
restate-server
```
Get the example:
```bash theme={null}
restate example typescript-tour-of-orchestration && cd typescript-tour-of-orchestration
npm install
```
Run the example:
```bash theme={null}
npm run dev
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Get the example:
```bash theme={null}
restate example java-tour-of-orchestration && cd java-tour-of-orchestration
```
Run the example:
```bash theme={null}
./gradlew run
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Get the example:
```bash theme={null}
restate example go-tour-of-orchestration && cd go-tour-of-orchestration
```
Run the example:
```bash theme={null}
go run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Get the example:
```bash theme={null}
restate example python-tour-of-orchestration && cd python-tour-of-orchestration
```
Run the example:
```bash theme={null}
uv run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Click in the UI's invocations tab on the inovcation ID of your request to see the execution trace of your request.
## Durable Execution
Restate uses Durable Execution to ensure your orchestration logic survives failures and restarts.
Whenever a handler executes an action with the Restate `Context`, this gets send over to the Restate Server and persisted in a log.
On a failure or a crash, the Restate Server sends a retry request that contains the log of the actions that were executed so far.
The service then replays the log to restore state and continues executing the remaining actions.
This process continues until the handler runs till completion.
**Key Benefits:**
* Context `run` actions make external calls or non-deterministic operations durable. They get replayed on failures.
* If the service crashes after payment creation, it resumes at the subscription step
* Deterministic IDs logged with the context ensure operations are idempotent
* Full execution traces for debugging and monitoring
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `failOnNetflix` from the `createSubscription` function in the `utils.ts` file:
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/utils.ts#subscription"} theme={null}
export function createSubscription(
userId: string,
subscription: string,
_paymentRef: string,
): string {
failOnNetflix(subscription);
terminalErrorOnDisney(subscription);
console.log(`>>> Created subscription ${subscription} for user ${userId}`);
return "SUCCESS";
}
```
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `failOnNetflix` from the `createSubscription` function in the `auxiliary/clients/SubscriptionClient.java` file:
```java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/auxiliary/clients/SubscriptionClient.java#subscription"} theme={null}
public static String createSubscription(String userId, String subscription, String paymentRef) {
failOnNetflix(subscription);
terminalErrorOnDisney(subscription);
System.out.println(">>> Created subscription " + subscription + " for user " + userId);
return "SUCCESS";
}
```
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `failOnNetflix` from the `CreateSubscription` function in the `utils.go` file:
```go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/utils.go#subscription"} theme={null}
func CreateSubscription(userId, subscription, paymentRef string) (string, error) {
if err := failOnNetflix(subscription); err != nil {
return "", err
}
if err := terminalErrorOnDisney(subscription); err != nil {
return "", err
}
fmt.Printf(">>> Created subscription %s for user %s\n", subscription, userId)
return "SUCCESS", nil
}
```
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `fail_on_netflix` from the `create_subscription` function in the `utils.py` file:
```python {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/utils.py#subscription"} theme={null}
def create_subscription(user_id: str, subscription: str, payment_ref: str) -> str:
fail_on_netflix(subscription)
terminal_error_on_disney(subscription)
print(f">>> Created subscription {subscription} for user {user_id}")
return "SUCCESS"
```
Once you restart the service, the workflow finishes successfully:
## Error Handling
By default, Restate retries failures infinitely with an exponential backoff strategy.
For some failures, you might not want to retry or only retry a limited number of times.
For these cases, Restate distinguishes between two types of errors:
* **Transient Errors**: These are temporary issues that can be retried, such as network timeouts or service unavailability. Restate automatically retries these errors.
* **Terminal Errors**: These indicate a failure that will not be retried, such as invalid input or business logic violations. Restate stops execution and allows you to handle these errors gracefully.
Throw a terminal error in your handler to indicate a terminal failure:
```typescript {"CODE_LOAD::ts/src/tour/microservices/terminal_error.ts#terminal_error"} theme={null}
throw new TerminalError("Invalid credit card");
```
```java {"CODE_LOAD::java/src/main/java/tour/microservices/ErrorHandler.java#here"} theme={null}
throw new TerminalException("Invalid credit card");
```
```go {"CODE_LOAD::go/tour/microservices/errorhandling.go#here"} theme={null}
return restate.ToTerminalError(fmt.Errorf("invalid credit card"))
```
```python {"CODE_LOAD::python/src/tour/microservices/terminal_error.py#here"} theme={null}
from restate.exceptions import TerminalError
raise TerminalError("Invalid credit card")
```
Some actions let you configure their retry behavior, for example to limit the number of retries of a run block:
```ts {"CODE_LOAD::ts/src/tour/microservices/retries.ts#retries"} theme={null}
const retryPolicy = {
maxRetryAttempts: 3,
initialRetryIntervalMillis: 1000,
};
const payRef = await ctx.run(
"pay",
() => createRecurringPayment(req.creditCard, paymentId),
retryPolicy
);
```
```java {"CODE_LOAD::java/src/main/java/tour/microservices/Retries.java#here"} theme={null}
RetryPolicy myRunRetryPolicy =
RetryPolicy.defaultPolicy().setInitialDelay(Duration.ofSeconds(1)).setMaxAttempts(3);
String payRef =
Restate.run(
"pay",
String.class,
myRunRetryPolicy,
() -> createRecurringPayment(req.creditCard(), paymentId));
```
```go {"CODE_LOAD::go/tour/microservices/retries.go#here"} theme={null}
result, err := restate.Run(ctx,
func(ctx restate.RunContext) (string, error) {
return createRecurringPayment(req.CreditCard, paymentId)
},
restate.WithInitialRetryInterval(time.Millisecond*100),
restate.WithMaxRetryAttempts(3),
restate.WithName("pay"),
)
if err != nil {
return err
}
```
```python {"CODE_LOAD::python/src/tour/microservices/retries.py#here"} theme={null}
pay_ref = await ctx.run_typed(
"pay",
lambda: create_recurring_payment(req["creditCard"], payment_id),
restate.RunOptions(max_attempts=10, max_retry_duration=timedelta(seconds=30)),
)
```
When the retries are exhausted, the run block will throw a `TerminalError`, that you can handle in your handler logic.
Learn more with the [Error Handling Guide](/guides/error-handling).
## Sagas and Rollback
On a terminal failure, Restate stops the execution of the handler.
You might, however, want to roll back the changes made by the workflow to keep your system in a consistent state.
This is where Sagas come in.
Sagas are a pattern for rolling back changes made by a handler when it fails.
In Restate, you can implement a saga by building a list of compensating actions for each step of the workflow.
On a terminal failure, you execute them in reverse order:
```ts src/sagas/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/sagas/service.ts?collapse_prequel"} theme={null}
export const subscriptionSaga = restate.service({
name: "SubscriptionSaga",
handlers: {
add: async (ctx: Context, req: SubscriptionRequest) => {
const compensations = [];
try {
const paymentId = ctx.rand.uuidv4();
compensations.push(() =>
ctx.run("undo-pay", () => removeRecurringPayment(paymentId)),
);
const payRef = await ctx.run("pay", () =>
createRecurringPayment(req.creditCard, paymentId),
);
for (const subscription of req.subscriptions) {
compensations.push(() =>
ctx.run(`undo-${subscription}`, () =>
removeSubscription(req.userId, subscription),
),
);
await ctx.run(`add-${subscription}`, () =>
createSubscription(req.userId, subscription, payRef),
);
}
} catch (e) {
if (e instanceof restate.TerminalError) {
for (const compensation of compensations.reverse()) {
await compensation();
}
}
throw e;
}
},
},
});
```
```java sagas/SubscriptionSaga.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/sagas/SubscriptionSaga.java?collapse_prequel"} theme={null}
@Service
public class SubscriptionSaga {
@Handler
public void add(SubscriptionRequest req) {
List compensations = new ArrayList<>();
try {
var paymentId = Restate.random().nextUUID().toString();
compensations.add(
() -> Restate.run("undo-pay", () -> PaymentClient.removeRecurringPayment(paymentId)));
String payRef =
Restate.run(
"pay",
String.class,
() -> PaymentClient.createRecurringPayment(req.creditCard(), paymentId));
for (String subscription : req.subscriptions()) {
compensations.add(
() ->
Restate.run(
"undo-" + subscription,
() -> SubscriptionClient.removeSubscription(req.userId(), subscription)));
Restate.run(
"add-" + subscription,
() -> SubscriptionClient.createSubscription(req.userId(), subscription, payRef));
}
} catch (TerminalException e) {
// Run compensations in reverse order
Collections.reverse(compensations);
for (Runnable compensation : compensations) {
compensation.run();
}
throw e;
}
}
}
```
```go sagas.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/sagas.go?collapse_prequel"} theme={null}
type SubscriptionSaga struct{}
func (SubscriptionSaga) Add(ctx restate.Context, req SubscriptionRequest) (err error) {
var compensations []func() error
// Run compensations at the end if err != nil
defer func() {
if err != nil {
for _, compensation := range slices.Backward(compensations) {
if compErr := compensation(); compErr != nil {
err = compErr
}
}
}
}()
paymentId := restate.UUID(ctx).String()
// Add compensation for payment
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return RemoveRecurringPayment(paymentId)
}, restate.WithName("undo-pay"))
return err
})
// Create payment
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateRecurringPayment(req.CreditCard, paymentId)
}, restate.WithName("pay"))
if err != nil {
return err
}
// Process subscriptions
for _, subscription := range req.Subscriptions {
// Add compensation for this subscription
sub := subscription // Capture loop variable
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return RemoveSubscription(req.UserId, sub)
}, restate.WithName(fmt.Sprintf("undo-%s", sub)))
return err
})
// Create subscription
_, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateSubscription(req.UserId, subscription, payRef)
}, restate.WithName(fmt.Sprintf("add-%s", subscription)))
if err != nil {
return err
}
}
return nil
}
```
```python app/sagas/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/sagas/service.py?collapse_prequel"} theme={null}
subscription_saga = restate.Service("SubscriptionSaga")
@subscription_saga.handler()
async def add(ctx: restate.Context, req: SubscriptionRequest) -> None:
compensations = []
try:
payment_id = str(ctx.uuid())
# Add compensation for payment
compensations.append(
lambda: ctx.run_typed(
"undo-pay", remove_recurring_payment, payment_id=payment_id
)
)
# Create payment
pay_ref = await ctx.run_typed(
"pay",
create_recurring_payment,
credit_card=req.credit_card,
payment_id=payment_id,
)
# Process subscriptions
for subscription in req.subscriptions:
# Add compensation for this subscription
compensations.append(
lambda s=subscription: ctx.run_typed( # type: ignore
f"undo-{s}",
remove_subscription,
user_id=req.user_id,
subscription=s,
)
)
# Create subscription
await ctx.run_typed(
f"add-{subscription}",
create_subscription,
user_id=req.user_id,
subscription=subscription,
payment_ref=pay_ref,
)
except restate.TerminalError as e:
# Run compensations in reverse order
for compensation in reversed(compensations):
await compensation()
raise e
```
**Benefits with Restate:**
* The list of compensations can be recovered after a crash, and Restate knows which compensations still need to be run.
* Sagas always run till completion (success or complete rollback)
* Full trace of all operations and compensations
* No complex state machines needed
Add a subscription for Disney:
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionSaga/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionSaga/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionSaga/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
```bash theme={null}
curl localhost:8080/restate/call/SubscriptionSaga/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
The Disney subscription is not available, so the handler will fail and run compensations:
Learn more with the [Sagas Guide](/guides/sagas).
## Virtual Objects
Until now, the services we looked at did not share any state between requests.
To implement stateful entities like shopping carts, user profiles, or AI agents, Restate provides **Virtual Objects**.
Each Virtual Object instance maintains isolated state and is identified by a unique key.
Here is an example of a Virtual Object that tracks user subscriptions:
```ts src/objects/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/objects/service.ts?collapse_prequel"} theme={null}
export const userSubscriptions = restate.object({
name: "UserSubscriptions",
handlers: {
add: async (ctx: ObjectContext, subscription: string) => {
// Get current subscriptions
const subscriptions = (await ctx.get("subscriptions")) ?? [];
// Add new subscription
if (!subscriptions.includes(subscription)) {
subscriptions.push(subscription);
}
ctx.set("subscriptions", subscriptions);
// Update metrics
ctx.set("lastUpdated", await ctx.date.toJSON());
},
getSubscriptions: restate.handlers.object.shared(
async (ctx: ObjectSharedContext) => {
return (await ctx.get("subscriptions")) ?? [];
},
),
},
});
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
```java objects/UserSubscriptions.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/objects/UserSubscriptions.java?collapse_prequel"} theme={null}
@VirtualObject
public class UserSubscriptions {
private static final StateKey> SUBSCRIPTIONS =
StateKey.of("subscriptions", new TypeRef<>() {});
private static final StateKey LAST_UPDATED = StateKey.of("lastUpdated", String.class);
@Handler
public void add(String subscription) {
var state = Restate.state();
// Get current subscriptions
Set subscriptions = state.get(SUBSCRIPTIONS).orElse(new HashSet<>());
// Add new subscription
subscriptions.add(subscription);
state.set(SUBSCRIPTIONS, subscriptions);
// Update metrics
state.set(LAST_UPDATED, Restate.instantNow().toString());
}
@Shared
public Set getSubscriptions() {
return Restate.state().get(SUBSCRIPTIONS).orElse(Set.of());
}
}
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
```go objects.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/objects.go?collapse_prequel"} theme={null}
type UserSubscriptions struct{}
func (UserSubscriptions) Add(ctx restate.ObjectContext, subscription string) error {
// Get current subscriptions
subscriptions, err := restate.Get[[]string](ctx, "subscriptions")
if err != nil {
return err
}
if subscriptions == nil {
subscriptions = []string{}
}
// Add new subscription if not already present
found := false
for _, sub := range subscriptions {
if sub == subscription {
found = true
break
}
}
if !found {
subscriptions = append(subscriptions, subscription)
}
// Save subscriptions
restate.Set(ctx, "subscriptions", subscriptions)
// Update metrics
restate.Set(ctx, "lastUpdated", time.Now().Format(time.RFC3339))
return nil
}
func (UserSubscriptions) GetSubscriptions(ctx restate.ObjectSharedContext) ([]string, error) {
return restate.Get[[]string](ctx, "subscriptions")
}
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `restate.Clear(ctx, "my-key")`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
```python app/objects/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/objects/service.py?collapse_prequel"} theme={null}
user_subscriptions = restate.VirtualObject("UserSubscriptions")
@user_subscriptions.handler()
async def add(ctx: restate.ObjectContext, subscription: str) -> None:
# Get current subscriptions
subscriptions = await ctx.get("subscriptions", type_hint=List[str]) or []
# Add new subscription
if subscription not in subscriptions:
subscriptions.append(subscription)
ctx.set("subscriptions", subscriptions)
# Update metrics
ctx.set("lastUpdated", datetime.now().isoformat())
@user_subscriptions.handler("getSubscriptions")
async def get_subscriptions(ctx: restate.ObjectSharedContext) -> List[str]:
return await ctx.get("subscriptions", type_hint=List[str]) or []
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
Add a few subscriptions for some users.
To call a Virtual Object, you specify the object key in the URL (here `user-123` and `user-456`):
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Hulu"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Prime"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Disney"'
curl localhost:8080/restate/call/UserSubscriptions/user-456/add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/getSubscriptions
```
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Hulu"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Prime"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Disney"'
curl localhost:8080/restate/call/UserSubscriptions/user-456/add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/getSubscriptions
```
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/Add --json '"Hulu"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/Add --json '"Prime"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/Add --json '"Disney"'
curl localhost:8080/restate/call/UserSubscriptions/user-456/Add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/GetSubscriptions
```
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Hulu"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Prime"'
curl localhost:8080/restate/call/UserSubscriptions/user-123/add --json '"Disney"'
curl localhost:8080/restate/call/UserSubscriptions/user-456/add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/restate/call/UserSubscriptions/user-123/getSubscriptions
```
Or use the UI's state tab to explore the object state.
## Resilient Communication
The Restate SDK includes clients to call other handlers reliably. You can call another handler in three ways:
* **Request-Response**: Wait for a response
* **One-Way Messages**: Fire-and-forget
* **Delayed Messages**: Schedule for later
When you call another handler, the Restate Server acts as a message broker.
All communication is proxied via the Restate Server where it gets durably logged and retried till completion.
Imagine a handler which processes a concert ticket purchase, and calls multiple services to handle payment, ticket delivery, and reminders:
```ts src/communication/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/communication/service.ts?collapse_prequel"} theme={null}
export const concertTicketingService = restate.service({
name: "ConcertTicketingService",
handlers: {
buy: async (ctx: Context, req: PurchaseTicketRequest) => {
// Request-response call - wait for payment to complete
const payRef = await ctx.serviceClient(paymentService).charge(req);
// One-way message - fire and forget ticket delivery
ctx.serviceSendClient(emailService).emailTicket(req);
// Delayed message - schedule reminder for day before concert
ctx
.serviceSendClient(emailService)
.sendReminder(req, sendOpts({ delay: dayBefore(req.concertDate) }));
return `Ticket purchased successfully with payment reference: ${payRef}`;
},
},
});
```
```java communication/ConcertTicketingService.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/communication/ConcertTicketingService.java?collapse_prequel"} theme={null}
@Service
public class ConcertTicketingService {
@Handler
public String buy(PurchaseTicketRequest req) {
// Request-response call - wait for payment to complete
String payRef = Restate.service(PaymentService.class).charge(req);
// One-way message - fire and forget ticket delivery
Restate.serviceHandle(EmailService.class).send(EmailService::emailTicket, req);
// Delayed message - schedule reminder for day before concert
Restate.serviceHandle(EmailService.class)
.send(EmailService::sendReminder, req, req.dayBefore());
return "Ticket purchased successfully with payment reference: " + payRef;
}
}
```
```go communication.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/communication.go?collapse_prequel"} theme={null}
type ConcertTicketingService struct{}
func (ConcertTicketingService) Buy(ctx restate.Context, req PurchaseTicketRequest) (string, error) {
// Request-response call - wait for payment to complete
payRef, err := restate.Service[string](ctx, "PaymentService", "Charge").Request(req)
if err != nil {
return "", err
}
// One-way message - fire and forget ticket delivery
restate.Service[restate.Void](ctx, "EmailService", "EmailTicket").Send(req)
// Delayed message - schedule reminder for day before concert
delay := DayBefore(req.ConcertDate)
restate.Service[restate.Void](ctx, "EmailService", "SendReminder").
Send(req, restate.WithDelay(delay))
return fmt.Sprintf("Ticket purchased successfully with payment reference: %s", payRef), nil
}
```
```python app/communication/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/communication/service.py?collapse_prequel"} theme={null}
concert_ticketing_service = restate.Service("ConcertTicketingService")
@concert_ticketing_service.handler()
async def buy(ctx: restate.Context, req: PurchaseTicketRequest) -> str:
# Request-response call - wait for payment to complete
pay_ref = await ctx.service_call(charge, req)
# One-way message - fire and forget ticket delivery
ctx.service_send(email_ticket, req)
# Delayed message - schedule reminder for day before concert
ctx.service_send(send_reminder_email, req, send_delay=day_before(req.concert_date))
return f"Ticket purchased successfully with payment reference: {pay_ref}"
```
Each of these calls gets persisted in Restate's log and will be retried upon failures.
The handler can finish execution without waiting for the ticket delivery or reminder to complete.
You can use Restate's communication primitives to implement microservices that communicate reliably and scale independently.
Buy a concert ticket:
```bash theme={null}
curl localhost:8080/restate/call/ConcertTicketingService/buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
```bash theme={null}
curl localhost:8080/restate/call/ConcertTicketingService/buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
```bash theme={null}
curl localhost:8080/restate/call/ConcertTicketingService/Buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
```bash theme={null}
curl localhost:8080/restate/call/ConcertTicketingService/buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
See in the UI how the first call had the response logged, while the ticket delivery happened asynchronously and the reminder was scheduled for in 406 days:
## Request Idempotency
Restate allows adding an idempotency header to your requests. It will then deduplicate requests with the same idempotency key, ensuring that they only execute once.
This can help us prevent duplicate calls the concert ticketing service if the user accidentally clicks "buy" multiple times.
Add an idempotency header to your request:
```shell theme={null}
curl -X POST localhost:8080/restate/call/ConcertTicketingService/buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
```shell theme={null}
curl -X POST localhost:8080/restate/call/ConcertTicketingService/buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
```shell theme={null}
curl -X POST localhost:8080/restate/call/ConcertTicketingService/Buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
```shell theme={null}
curl -X POST localhost:8080/restate/call/ConcertTicketingService/buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
Notice how doing the same request with the same idempotency key will print the same payment reference.
Instead of executing the handler again, Restate returns the result of the first execution.
## External Events
Until now we showed either synchronous API calls via `run` or calls to other Restate services.
Another common scenario is APIs that respond asynchronously via webhooks or callbacks.
For this, you can create a durable promise with Restate's awakeable API.
For example, some payment providers like Stripe require you to initiate a payment and then wait for their webhook to confirm the transaction.
```ts src/events/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/events/service.ts?collapse_prequel"} theme={null}
export const payments = restate.service({
name: "Payments",
handlers: {
process: async (ctx: Context, req: PaymentRequest) => {
// Create awakeable to wait for webhook payment confirmation
const confirmation = ctx.awakeable();
// Initiate payment with external provider (Stripe, PayPal, etc.)
const paymentId = ctx.rand.uuidv4();
await ctx.run("pay", () => initPayment(req, paymentId, confirmation.id));
// Wait for external payment provider to call our webhook
return confirmation.promise;
},
// Webhook handler called by external payment provider
confirm: async (
ctx: Context,
confirmation: { id: string; result: PaymentResult },
) => {
// Resolve the awakeable to continue the payment flow
ctx.resolveAwakeable(confirmation.id, confirmation.result);
},
},
});
```
```java events/Payments.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/events/Payments.java?collapse_prequel"} theme={null}
@Service
public class Payments {
@Handler
public PaymentResult process(PaymentRequest req) {
// Create awakeable to wait for webhook payment confirmation
var confirmation = Restate.awakeable(PaymentResult.class);
// Initiate payment with external provider (Stripe, PayPal, etc.)
var paymentId = Restate.random().nextUUID().toString();
Restate.run("pay", () -> initPayment(req, paymentId, confirmation.id()));
// Wait for external payment provider to call our webhook
return confirmation.await();
}
// Webhook handler called by external payment provider
@Handler
public void confirm(ConfirmationRequest confirmation) {
// Resolve the awakeable to continue the payment flow
Restate.awakeableHandle(confirmation.id()).resolve(PaymentResult.class, confirmation.result());
}
}
```
```go events.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/events.go?collapse_prequel"} theme={null}
type Payments struct{}
func (Payments) Process(ctx restate.Context, req PaymentRequest) (PaymentResult, error) {
// Create awakeable to wait for webhook payment confirmation
confirmation := restate.Awakeable[PaymentResult](ctx)
// Initiate payment with external provider (Stripe, PayPal, etc.)
paymentId := restate.UUID(ctx).String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return InitPayment(req, paymentId, confirmation.Id())
}, restate.WithName("pay"))
if err != nil {
return PaymentResult{}, err
}
// Wait for external payment provider to call our webhook
return confirmation.Result()
}
// Webhook handler called by external payment provider
func (Payments) Confirm(ctx restate.Context, confirmation ConfirmationRequest) error {
// Resolve the awakeable to continue the payment flow
restate.ResolveAwakeable(ctx, confirmation.Id, confirmation.Result)
return nil
}
```
```python app/events/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/events/service.py?collapse_prequel"} theme={null}
payments = restate.Service("Payments")
@payments.handler()
async def process(ctx: restate.Context, req: PaymentRequest) -> PaymentResult:
# Create awakeable to wait for webhook payment confirmation
confirmation_id, confirmation_promise = ctx.awakeable(type_hint=PaymentResult)
# Initiate payment with external provider (Stripe, PayPal, etc.)
payment_id = str(ctx.uuid())
await ctx.run_typed(
"pay",
init_payment,
req=req,
payment_id=payment_id,
confirmation_id=confirmation_id,
)
# Wait for external payment provider to call our webhook
return await confirmation_promise
@payments.handler()
async def confirm(ctx: restate.Context, confirmation: ConfirmationRequest) -> None:
# Resolve the awakeable to continue the payment flow
ctx.resolve_awakeable(confirmation.id, confirmation.result)
```
The awakeable API creates a durable promise or future that can be recovered after a crash.
Restate persists it in its log and can recover it on another process when needed.
There is no limit to how long you can wait, so external events may take hours or even months to arrive.
You can also use this pattern for human-in-the-loop interactions, such as waiting for user input or approvals.
Initiate a payment by calling the `process` handler. Use the `send` verb to call the handler without waiting for the response:
```bash theme={null}
curl localhost:8080/restate/send/Payments/process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/restate/send/Payments/process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/restate/send/Payments/Process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/restate/send/Payments/process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
In the UI, you can see that the payment is waiting for confirmation.
You can restart the service to see how Restate continues waiting for the payment confirmation.
Simulate approving the payment by executing the **curl request that was printed in the service logs**, similar to:
```bash theme={null}
curl localhost:8080/restate/call/Payments/confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
```bash theme={null}
curl localhost:8080/restate/call/Payments/confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
```bash theme={null}
curl localhost:8080/restate/call/Payments/Confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
```bash theme={null}
curl localhost:8080/restate/call/Payments/confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
You can see in the UI that the payment was processed successfully and the awakeable was resolved:
## Durable Timers
Waiting on external events might take a long time, and you might want to add timeouts to operations like this.
The Restate SDK offers durable timer implementations that you can use to limit waiting for an action.
Restate tracks these timers so they survive crashes and do not restart from the beginning.
Let's extend our payment service to automatically cancel payments that don't complete within a reasonable time:
```ts src/timers/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/timers/service.ts?collapse_prequel"} theme={null}
export const paymentsWithTimeout = restate.service({
name: "PaymentsWithTimeout",
handlers: {
process: async (ctx: Context, req: PaymentRequest) => {
const confirmation = ctx.awakeable();
const paymentId = ctx.rand.uuidv4();
const payRef = await ctx.run("pay", () =>
initPayment(req, paymentId, confirmation.id),
);
// Race between payment confirmation and timeout
try {
return await confirmation.promise.orTimeout({ seconds: 30 });
} catch (e) {
if (e instanceof TimeoutError) {
// Cancel the payment with external provider
await ctx.run("cancel-payment", () => cancelPayment(payRef));
return {
success: false,
errorMessage: "Payment timeout",
};
}
throw e;
}
},
confirm: async (
ctx: Context,
confirmation: { id: string; result: PaymentResult },
) => {
ctx.resolveAwakeable(confirmation.id, confirmation.result);
},
},
});
```
```java timers/PaymentsWithTimeout.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/timers/PaymentsWithTimeout.java?collapse_prequel"} theme={null}
@Service
public class PaymentsWithTimeout {
@Handler
public PaymentResult process(PaymentRequest req) {
var confirmation = Restate.awakeable(PaymentResult.class);
var paymentId = Restate.random().nextUUID().toString();
String payRef =
Restate.run("pay", String.class, () -> initPayment(req, paymentId, confirmation.id()));
try {
return confirmation.await(Duration.ofSeconds(30));
} catch (TimeoutException e) {
Restate.run("cancel-payment", () -> cancelPayment(payRef));
return new PaymentResult(false, null, "Payment timeout");
}
}
@Handler
public void confirm(ConfirmationRequest confirmation) {
Restate.awakeableHandle(confirmation.id()).resolve(PaymentResult.class, confirmation.result());
}
}
```
```go timers.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/timers.go?collapse_prequel"} theme={null}
type PaymentsWithTimeout struct{}
func (PaymentsWithTimeout) Process(ctx restate.Context, req PaymentRequest) (PaymentResult, error) {
confirmation := restate.Awakeable[PaymentResult](ctx)
paymentId := restate.UUID(ctx).String()
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return InitPayment(req, paymentId, confirmation.Id())
}, restate.WithName("pay"))
if err != nil {
return PaymentResult{}, err
}
// Race between payment confirmation and timeout
timeout := restate.After(ctx, 30*time.Second)
resFut, err := restate.WaitFirst(ctx, confirmation, timeout)
if err != nil {
return PaymentResult{}, err
}
switch resFut {
case confirmation:
return confirmation.Result()
default:
if err := timeout.Done(); err != nil {
return PaymentResult{}, err
}
// Cancel the payment with external provider
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return CancelPayment(payRef)
}, restate.WithName("cancel-payment"))
if err != nil {
return PaymentResult{}, err
}
return PaymentResult{
Success: false,
ErrorMessage: "Payment timeout",
}, nil
}
}
func (PaymentsWithTimeout) Confirm(ctx restate.Context, confirmation ConfirmationRequest) error {
restate.ResolveAwakeable(ctx, confirmation.Id, confirmation.Result)
return nil
}
```
```python app/timers/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/timers/service.py?collapse_prequel"} theme={null}
payments_with_timeout = restate.Service("PaymentsWithTimeout")
@payments_with_timeout.handler()
async def process(ctx: restate.Context, req: PaymentRequest) -> PaymentResult:
confirmation_id, confirmation_promise = ctx.awakeable(type_hint=PaymentResult)
payment_id = str(ctx.uuid())
pay_ref = await ctx.run_typed(
"pay",
init_payment,
req=req,
payment_id=payment_id,
confirmation_id=confirmation_id,
)
# Race between payment confirmation and timeout
match await restate.select(
confirmation=confirmation_promise, timeout=ctx.sleep(timedelta(seconds=30))
):
case ["confirmation", result]:
return result
case _:
# Cancel the payment with external provider
await ctx.run_typed("cancel-payment", cancel_payment, pay_ref=pay_ref)
return PaymentResult(
success=False, transaction_id=None, error_message="Payment timeout"
)
@payments_with_timeout.handler()
async def confirm(ctx: restate.Context, confirmation: ConfirmationRequest) -> None:
ctx.resolve_awakeable(confirmation.id, confirmation.result)
```
You can also set timeouts for RPC calls or other asynchronous operations with the Restate SDK.
Initiate a payment by calling the `process` handler. Use the `send` verb to call the handler without waiting for the response:
```bash theme={null}
curl localhost:8080/restate/send/PaymentsWithTimeout/process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/restate/send/PaymentsWithTimeout/process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/restate/send/PaymentsWithTimeout/Process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/restate/send/PaymentsWithTimeout/process \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
Wait for 30 seconds without confirming the payment.
Try restarting the service while the payment is waiting for confirmation to see how Restate continues waiting for the timer and the confirmation.
In the UI, you can see that the payment times out and cancels the payment:
## Concurrent Tasks
When you are waiting on an awakeable or a timer, you are effectively running concurrent tasks and waiting for one of them to complete.
Restate allows more advanced concurrency patterns to run tasks in parallel and wait for their results.
Let's extend our subscription service to process all subscriptions concurrently and handle failures gracefully:
```ts src/concurrenttasks/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/concurrenttasks/service.ts?collapse_prequel"} theme={null}
export const parallelSubscriptionService = restate.service({
name: "ParallelSubscriptionService",
handlers: {
add: async (ctx: Context, req: SubscriptionRequest) => {
const paymentId = ctx.rand.uuidv4();
const payRef = await ctx.run("pay", () =>
createRecurringPayment(req.creditCard, paymentId),
);
// Start all subscriptions in parallel
const subscriptionPromises = [];
for (const subscription of req.subscriptions) {
subscriptionPromises.push(
ctx.run(`add-${subscription}`, () =>
createSubscription(req.userId, subscription, payRef),
),
);
}
// Wait for all subscriptions to complete
await RestatePromise.all(subscriptionPromises);
return { success: true, paymentRef: payRef };
},
},
});
```
```java concurrenttasks/ParallelSubscriptionService.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/concurrenttasks/ParallelSubscriptionService.java?collapse_prequel"} theme={null}
@Service
public class ParallelSubscriptionService {
@Handler
public SubscriptionResult add(SubscriptionRequest req) {
var paymentId = Restate.random().nextUUID().toString();
var payRef =
Restate.run("pay", String.class, () -> createRecurringPayment(req.creditCard(), paymentId));
// Start all subscriptions in parallel
List> subscriptionFutures = new ArrayList<>();
for (String subscription : req.subscriptions()) {
subscriptionFutures.add(
Restate.runAsync(
"add-" + subscription, () -> createSubscription(req.userId(), subscription, payRef)));
}
// Wait for all subscriptions to complete
DurableFuture.all(subscriptionFutures).await();
return new SubscriptionResult(true, payRef);
}
}
```
```go concurrenttasks.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/concurrenttasks.go?collapse_prequel"} theme={null}
type ParallelSubscriptionService struct{}
func (ParallelSubscriptionService) Add(ctx restate.Context, req SubscriptionRequest) (SubscriptionResult, error) {
paymentId := restate.UUID(ctx).String()
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateRecurringPayment(req.CreditCard, paymentId)
}, restate.WithName("pay"))
if err != nil {
return SubscriptionResult{}, err
}
// Process all subscriptions sequentially
var subscriptionFutures []restate.Future
for _, subscription := range req.Subscriptions {
future := restate.RunAsync(ctx, func(ctx restate.RunContext) (string, error) {
return CreateSubscription(req.UserId, subscription, payRef)
}, restate.WithName(fmt.Sprintf("add-%s", subscription)))
subscriptionFutures = append(subscriptionFutures, future)
}
for fut, err := range restate.Wait(ctx, subscriptionFutures...) {
if err != nil {
return SubscriptionResult{}, err
}
_, err := fut.(restate.RunAsyncFuture[string]).Result()
if err != nil {
return SubscriptionResult{}, err
}
}
return SubscriptionResult{
Success: true,
PaymentRef: payRef,
}, nil
}
```
```python app/concurrenttasks/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/concurrenttasks/service.py?collapse_prequel"} theme={null}
parallel_subscription_service = restate.Service("ParallelSubscriptionService")
@parallel_subscription_service.handler()
async def add(ctx: restate.Context, req: SubscriptionRequest) -> SubscriptionResult:
payment_id = str(ctx.uuid())
pay_ref = await ctx.run_typed(
"pay",
create_recurring_payment,
credit_card=req.credit_card,
payment_id=payment_id,
)
# Start all subscriptions in parallel
subscription_tasks = []
for subscription in req.subscriptions:
task = ctx.run_typed(
f"add-{subscription}",
create_subscription,
user_id=req.user_id,
subscription=subscription,
payment_ref=pay_ref,
)
subscription_tasks.append(task)
# Wait for all subscriptions to complete
await restate.gather(*subscription_tasks)
return SubscriptionResult(success=True, payment_ref=pay_ref)
```
Restate retries all parallel tasks until they all complete and can deterministically replay the order of completion.
Add a few subscriptions for some users.
```bash theme={null}
curl localhost:8080/restate/call/ParallelSubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
```bash theme={null}
curl localhost:8080/restate/call/ParallelSubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
```bash theme={null}
curl localhost:8080/restate/call/ParallelSubscriptionService/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
```bash theme={null}
curl localhost:8080/restate/call/ParallelSubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
In the UI, you can see that all subscriptions are processed in parallel:
You can extend this to include the saga pattern and run all compensations in parallel as well.
Have a look at the Concurrent Tasks docs for your SDK to learn more ([TS](/develop/ts/concurrent-tasks) / [Java / Kotlin](/develop/java/concurrent-tasks) / [Python](/develop/python/concurrent-tasks) / [Go](/develop/go/concurrent-tasks)).
## Summary
Restate simplifies microservice orchestration with:
* **Durable Execution**: Automatic failure recovery without complex retry logic
* **Sagas**: Distributed transactions with resilient compensation
* **Service Communication**: Reliable RPC and messaging between services
* **Stateful Processing**: Consistent state management without external stores
* **Advanced Patterns**: Fault-tolerant timers, awakeables, and parallel execution
Build resilient distributed systems without the typical complexity.
# Workflows
Source: https://docs.restate.dev/tour/workflows
Build resilient workflows with familiar programming patterns.
Workflows orchestrate complex business processes that span multiple steps, services, and time periods. Restate workflows are written as regular functions in your programming language, with automatic durability, state management, and event handling built in.
In this guide, you'll learn how to:
* Write workflows as regular functions with automatic durability
* Handle long-running processes with state and event patterns
* Deploy steps inline or services that can scale independently
* Build resilient, observable workflows without external dependencies
## Getting Started
A Restate application is composed of two main components:
* **Restate Server**: The core engine that manages durable execution and orchestrates services. It acts as a message broker or reverse proxy in front of your services.
* **Your Services**: Your workflows and business logic, implemented as service handlers using the Restate SDK to perform durable operations.
A basic signup workflow looks like this:
```typescript signup-workflow.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-workflow.ts?collapse_prequel"} theme={null}
export const signupWorkflow = restate.workflow({
name: "SignupWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key; // workflow ID = user ID
// Write to database
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) return { success };
// Call APIs
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success };
},
},
});
```
```java SignupWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWorkflow {
@Workflow
public boolean run(User user) {
String userId = Restate.key(); // workflow ID = user ID
// Write to database
boolean success = Restate.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
return false;
}
// Call APIs
Restate.run("activate", () -> activateUser(userId));
Restate.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
}
```
```go getstarted.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/getstarted.go?collapse_prequel"} theme={null}
type SignupWorkflow struct{}
func (SignupWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx) // workflow ID = user ID
// Write to database
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil || !success {
return false, err
}
// Call APIs
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
return true, nil
}
```
```python signup_workflow.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_workflow.py?collapse_prequel"} theme={null}
signup_workflow = restate.Workflow("SignupWorkflow")
@signup_workflow.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key() # workflow ID = user ID
# Write to database
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
return False
# Call APIs
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return True
```
A workflow has handlers that can be called over HTTP.
The `run` handler is the main entry point that executes the workflow logic.
An execution of the workflow is identified by a unique key (in this case, the user ID)
and uses Restate's `WorkflowContext` to make steps durable.
You don't need to run your services in any special way. Restate works with how you already deploy your code, whether that's in Docker, on Kubernetes, or via AWS Lambda.
The endpoint that serves the workflows of this tour over HTTP is defined in `src/app.ts`.
The endpoint that serves the workflows of this tour over HTTP is defined in `AppMain.java`.
The endpoint that serves the workflows of this tour over HTTP is defined in `main.go`.
The endpoint that serves the workflows of this tour over HTTP is defined in `__main__.py`.
### Run the example
[Install Restate](/installation) and launch it:
```bash theme={null}
restate-server
```
Get the example:
```bash theme={null}
restate example typescript-tour-of-workflows && cd typescript-tour-of-workflows
npm install
```
Run the example:
```bash theme={null}
npm run dev
```
Then, tell Restate where your workflow is running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
Get the example:
```bash theme={null}
restate example java-tour-of-workflows && cd java-tour-of-workflows
```
Run the example:
```bash theme={null}
./gradlew run
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
Get the example:
```bash theme={null}
restate example go-tour-of-workflows && cd go-tour-of-workflows
```
Run the example:
```bash theme={null}
go run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
Get the example:
```bash theme={null}
restate example python-tour-of-workflows && cd python-tour-of-workflows
```
Run the example:
```bash theme={null}
uv run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of workflows that we will be covering in this tutorial.
## Submitting Workflows
The workflow can be submitted over HTTP, Kafka, programmatically, or via the UI.
To submit the workflow via HTTP send the request to `restate-ingress/workflow-name/key/run`, in our case:
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/johndoe/Run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Restate deduplicates workflow executions on the key, here `johndoe`.
Resubmission of the same workflow will fail with "Previously accepted". The invocation ID can be found in the request header `x-restate-id` (add `-v` to your request).
To try out a workflow multiple times during the tour, use a different key.
You can invoke a workflow programmatically with the Restate SDK:
```ts client.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/client.ts#submit"} theme={null}
const restateClient = clients.connect({ url: "http://localhost:8080" });
const handle = await restateClient
.workflowClient(signupWorkflow, id)
.workflowSubmit({ name, email });
const result = await restateClient.result(handle);
```
The workflow gets submitted and afterwards you can retrieve the result by attaching to it.
Run the client script via:
```bash theme={null}
npm run client
```
You can invoke a workflow programmatically with the Restate SDK:
```java WorkflowSubmitter.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/WorkflowSubmitter.java#submit"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
boolean result =
restateClient
.workflowHandle(SignupWorkflow.class, "user-123")
.send(SignupWorkflow::run, user)
.attach()
.response();
```
The workflow gets submitted and afterwards you can retrieve the result by attaching to it.
Run the client script via:
```bash theme={null}
./gradlew -PmainClass=my.example.WorkflowSubmitter run
```
You can invoke a workflow programmatically with the Restate SDK:
```go client.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/client/client.go#submit"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
result, err := restateingress.Workflow[utils.User, bool](
restateClient, "SignupWorkflow", "user-123", "Run").
Request(context.Background(), user)
```
The workflow gets submitted and afterwards you can retrieve the result by attaching to it.
Run the client script via:
```bash theme={null}
go run ./client
```
You can invoke a workflow programmatically by sending an HTTP request:
```python client.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/client.py#submit"} theme={null}
key = "user-123"
url = "http://127.0.0.1:8080/SignupWorkflow/" + key + "/run"
payload = {"name": "John Doe", "email": "john@mail.com"}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
response = httpx.post(url, json=payload, headers=headers)
```
Run the client script via:
```bash theme={null}
uv run client.py
```
You can schedule a workflow to run at a later time by specifying a delay:
```bash theme={null}
curl "localhost:8080/restate/send/SignupWorkflow/petewhite/run?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
```bash theme={null}
curl "localhost:8080/restate/send/SignupWorkflow/petewhite/run?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
```bash theme={null}
curl "localhost:8080/restate/send/SignupWorkflow/petewhite/Run?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
```bash theme={null}
curl "localhost:8080/restate/send/SignupWorkflow/petewhite/run?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
There is no limit to how long you can delay a workflow (works for months, even years).
Have a look at the SDK docs to learn how to schedule workflows programmatically ([TS](/services/invocation/clients/typescript-sdk) / [Java](/services/invocation/clients/java-sdk) / [Python](/services/invocation/clients/python-sdk) / [Go](/services/invocation/clients/go-sdk)).
If a workflow is already ongoing, you can also attach to it to get the result once it finishes:
```bash theme={null}
curl localhost:8080/restate/attach \
--json '{"target": "workflow", "workflowName": "SignupWorkflow", "workflowKey": "johndoe"}'
```
Have a look at the SDK docs to learn how to attach to workflows programmatically ([TS](/services/invocation/clients/typescript-sdk) / [Java](/services/invocation/clients/java-sdk) / [Go](/services/invocation/clients/go-sdk)).
## Durable Execution
Restate uses Durable Execution to ensure your business logic survives any failure and resumes exactly where it left off. Unlike traditional workflow systems that require separate orchestrator infrastructure and worker management, Restate lets you deploy your workflows the same way you deploy your application code.
You write a workflow as a regular function. You use the Restate SDK to persist the steps your workflow completes in the Restate Server.
If your workflow crashes or restarts, the execution replays from the journal to restore state and continue processing:
To persist a workflow step, you use the `WorkflowContext` actions:
* **Durable Steps**: Restate's run actions ensures non-deterministic operations like database writes or external API calls are persisted
* **Progress Recovery**: If the workflow crashes after user creation, it resumes at the email step
* **Observability**: Full execution traces for debugging and monitoring
Send a request for Alice:
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/alicedoe/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/alicedoe/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/alicedoe/Run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWorkflow/alicedoe/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
Go to the UI at `http://localhost:9070`, on the invocations page, and click on the invocation ID of the retrying invocation:
You see how the invocation went through the steps of the workflow, and how it is stuck on retrying to send the welcome email.
To fix the problem, remove the line `failOnAlice` from the `sendWelcomeEmail` function in the `utils.ts` file:
```ts utils.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/utils.ts#here"} theme={null}
export function sendWelcomeEmail(user: User) {
failOnAlice(user.name, "send welcome email");
console.log(`Welcome email sent: ${user.email}`);
}
```
To fix the problem, remove the line `failOnAlice` from the `sendWelcomeEmail` function in the `Utils.java` file:
```java Utils.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/utils/Utils.java#here"} theme={null}
private static void terminalErrorOnAlice(String name, String action) {
if ("Alice".equals(name)) {
String message =
"[👻 SIMULATED] Failed to " + action + " for " + name + ": not available in this country";
System.err.println(message);
throw new TerminalException(message);
}
}
```
To fix the problem, remove the line `failOnAlice` from the `sendWelcomeEmail` function in the `utils.go` file:
```go utils.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/utils.go#here"} theme={null}
func SendWelcomeEmail(user User) (restate.Void, error) {
if err := failOnAlice(user.Name, "send welcome email"); err != nil {
return restate.Void{}, err
}
fmt.Printf("Welcome email sent: %s\n", user.Email)
return restate.Void{}, nil
}
```
To fix the problem, remove the line `fail_on_alice` from the `send_welcome_email` function in the `utils.py` file:
```python utils.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/utils.py#here"} theme={null}
def send_welcome_email(user: User):
fail_on_alice(user.name, "send welcome email")
print(f"Welcome email sent: {user.email}")
```
Once you restart the service, the workflow finishes successfully:
## In-line Steps vs. Separate Activities
Restate workflows can execute operations inline or delegate to separate services, giving you flexibility in how you structure your applications.
* **In-line Steps** - Execute directly in the workflow, for example a run block.
* **Separate Activities** - Call dedicated services for independent scaling, separation of concerns, or different concurrency requirements.
```ts signup-with-activities.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-activities.ts#activities"} theme={null}
// Move user DB interaction to dedicated service
const success = await ctx
.serviceClient(userService)
.createUser({ userId, user });
if (!success) return { success };
// Execute other steps inline
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
```
```java SignupWithActivitiesWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithActivitiesWorkflow.java#activities"} theme={null}
// Move user DB interaction to dedicated service
boolean success = true;
Restate.service(UserService.class).createUser(new CreateUserRequest(userId, user));
if (!success) {
return false;
}
// Execute other steps inline
Restate.run("activate", () -> activateUser(userId));
Restate.run("welcome", () -> sendWelcomeEmail(user));
```
```go activities.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/activities.go#activities"} theme={null}
// Move user DB interaction to dedicated service
success, err := restate.Service[bool](ctx, "UserService", "CreateUser").
Request(CreateUserRequest{UserID: userID, User: user})
if err != nil || !success {
return false, err
}
// Execute other steps inline
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
```
```python signup_with_activities.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_activities.py#activities"} theme={null}
# Move user DB interaction to dedicated service
success = await ctx.service_call(
create_user_handler, arg=CreateUserRequest(user_id=user_id, user=user)
)
if not success:
return False
# Execute other steps inline
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
```
You can also use this to nest workflows. The main workflow can call other workflows as activities.
Workflows are just one of the service types Restate supports. The other service types are:
* [Services](/foundations/services): collections of independent handlers which get executed with Durable Execution.
* [Virtual Objects](/foundations/services): stateful services that can be used to manage state and concurrency across multiple invocations.
To learn more, follow at the [Microservice Orchestration Tour](/tour/microservice-orchestration).
Submit the workflow:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithActivitiesWorkflow/carl/run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithActivitiesWorkflow/carl/run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithActivitiesWorkflow/carl/Run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithActivitiesWorkflow/carl/run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
In the UI, you can see how the invocation called another service called user service:
## Workflow Patterns
Restate provides powerful patterns for building complex workflows using familiar programming constructs.
### Querying Workflow State
Workflows can store state in Restate, which can be queried later by other handlers:
```ts signup-with-queries.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-queries.ts?collapse_prequel"} theme={null}
export const signupWithQueries = restate.workflow({
name: "SignupWithQueriesWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
ctx.set("user", user);
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) {
ctx.set("status", "failed");
return { success };
}
ctx.set("status", "created");
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success };
},
getStatus: async (ctx: WorkflowSharedContext) => {
return {
status: await ctx.get("status"),
user: await ctx.get("user"),
};
},
},
});
```
```java SignupWithQueriesWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithQueriesWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithQueriesWorkflow {
private static final StateKey USER = StateKey.of("user", User.class);
private static final StateKey STATUS = StateKey.of("status", String.class);
@Workflow
public boolean run(User user) {
String userId = Restate.key();
var state = Restate.state();
state.set(USER, user);
boolean success = Restate.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
state.set(STATUS, "failed");
return false;
}
state.set(STATUS, "created");
Restate.run("activate", () -> activateUser(userId));
Restate.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
@Shared
public StatusResponse getStatus() {
var state = Restate.state();
String status = state.get(STATUS).orElse("unknown");
User user = state.get(USER).orElse(null);
return new StatusResponse(status, user);
}
}
```
```go queries.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/queries.go?collapse_prequel"} theme={null}
type SignupWithQueriesWorkflow struct{}
func (SignupWithQueriesWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
restate.Set(ctx, "user", user)
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil || !success {
restate.Set(ctx, "status", "failed")
return false, err
}
restate.Set(ctx, "status", "created")
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
return success, nil
}
func (SignupWithQueriesWorkflow) GetStatus(ctx restate.WorkflowSharedContext) (StatusResponse, error) {
status, err := restate.Get[string](ctx, "status")
if err != nil {
return StatusResponse{}, err
}
user, err := restate.Get[User](ctx, "user")
if err != nil {
return StatusResponse{}, err
}
return StatusResponse{Status: &status, User: &user}, nil
}
```
```python signup_with_queries.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_queries.py?collapse_prequel"} theme={null}
signup_with_queries = restate.Workflow("SignupWithQueriesWorkflow")
@signup_with_queries.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
ctx.set("user", user.model_dump())
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
ctx.set("status", "failed")
return False
ctx.set("status", "created")
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return True
@signup_with_queries.handler("getStatus")
async def get_status(ctx: WorkflowSharedContext) -> StatusResponse:
return StatusResponse(status=await ctx.get("status"), user=await ctx.get("user"))
```
Key characteristics:
* State is isolated per workflow execution.
* State lives up to the duration of the workflow retention ([default one day](/services/configuration)).
* State is queryable from other handlers or the Restate UI.
Submit the workflow:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithQueriesWorkflow/janedoe/run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithQueriesWorkflow/janedoe/run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithQueriesWorkflow/janedoe/Run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithQueriesWorkflow/janedoe/run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
In the UI, look at the state tab and filter on the `SignupWithQueriesWorkflow`.
### Workflow promises
Pause workflow execution while it waits for an external event using a durable promise:
```ts signup-with-signals.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-signals.ts?collapse_prequel"} theme={null}
export const signupWithSignals = restate.workflow({
name: "SignupWithSignalsWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
// Generate verification secret and send email
const secret = ctx.rand.uuidv4();
await ctx.run("verify", () =>
sendVerificationEmail(userId, user, secret),
);
// Wait for user to click verification link
const clickedSecret = await ctx.promise("email-verified");
return { success: clickedSecret === secret };
},
verifyEmail: async (
ctx: WorkflowSharedContext,
req: { secret: string },
) => {
// Resolve the promise to continue the main workflow
await ctx.promise("email-verified").resolve(req.secret);
},
},
});
```
```java SignupWithSignalsWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithSignalsWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithSignalsWorkflow {
private static final DurablePromiseKey EMAIL_VERIFIED_PROMISE =
DurablePromiseKey.of("email-verified", String.class);
@Workflow
public boolean run(User user) {
String userId = Restate.key();
// Generate verification secret and send email
String secret = Restate.random().nextUUID().toString();
Restate.run("verify", () -> sendVerificationEmail(userId, user, secret));
// Wait for user to click verification link
String clickedSecret = Restate.promise(EMAIL_VERIFIED_PROMISE).future().await();
return secret.equals(clickedSecret);
}
@Shared
public void verifyEmail(VerifyEmailRequest req) {
Restate.promiseHandle(EMAIL_VERIFIED_PROMISE).resolve(req.secret());
}
}
```
```go signals.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/signals.go?collapse_prequel"} theme={null}
type SignupWithSignalsWorkflow struct{}
func (SignupWithSignalsWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
// Generate verification secret and send email
secret := restate.UUID(ctx).String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendVerificationEmail(userID, user, secret)
}, restate.WithName("verify"))
if err != nil {
return false, err
}
// Wait for user to click verification link
clickedSecret, err := restate.Promise[string](ctx, "email-verified").Result()
if err != nil {
return false, err
}
return clickedSecret == secret, nil
}
func (SignupWithSignalsWorkflow) VerifyEmail(ctx restate.WorkflowSharedContext, req VerifyEmailRequest) error {
// Resolve the promise to continue the main workflow
return restate.Promise[string](ctx, "email-verified").Resolve(req.Secret)
}
```
```python signup_with_signals.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_signals.py?collapse_prequel"} theme={null}
signup_with_signals = restate.Workflow("SignupWithSignalsWorkflow")
@signup_with_signals.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
# Generate verification secret and send email
secret = str(ctx.uuid())
await ctx.run_typed(
"verify",
send_verification_email,
user_id=user_id,
user=user,
verification_secret=secret,
)
# Wait for user to click verification link
clicked_secret = await ctx.promise("email-verified", type_hint=str).value()
return clicked_secret == secret
@signup_with_signals.handler("verifyEmail")
async def verify_email(ctx: WorkflowSharedContext, req: VerifyEmailRequest) -> None:
# Resolve the promise to continue the main workflow
await ctx.promise("email-verified", type_hint=str).resolve(req.secret)
```
The promise survives restarts and crashes and can be recovered on another process.
You can use these durable promises to handle asynchronous events without complex message queues or external state management.
Promises can be resolved before the workflow waits for them, avoiding complex synchronization issues.
Submit the workflow asynchronously with the `send` verb:
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/johndoe/Run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
In the UI, you can see the workflow waiting for the `email-verified` promise to be resolved.
Try killing the service and restarting it. The workflow will continue waiting for the promise to be resolved.
To resolve the promise, **copy over the curl request from the service logs**, which looks like this:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSignalsWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSignalsWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSignalsWorkflow/johndoe/VerifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSignalsWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
Now the UI will show the workflow completed successfully.
### Workflow Events
You can also use promises in the other direction: the run handler resolves a promise, and another handler waits for its result:
```ts signup-with-events.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-events.ts?collapse_prequel"} theme={null}
export const signupWithEvents = restate.workflow({
name: "SignupWithEventsWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) {
await ctx.promise("user-created").reject("Creation failed.");
return { success };
}
await ctx.promise("user-created").resolve("User created.");
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success };
},
waitForUserCreation: async (ctx: WorkflowSharedContext) => {
return ctx.promise("user-created");
},
},
});
```
```java SignupWithEventsWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithEventsWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithEventsWorkflow {
private static final DurablePromiseKey USER_CREATED_PROMISE =
DurablePromiseKey.of("user-created", String.class);
@Workflow
public boolean run(User user) {
String userId = Restate.key();
boolean success = Restate.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
Restate.promiseHandle(USER_CREATED_PROMISE).reject("Creation failed.");
return false;
}
Restate.promiseHandle(USER_CREATED_PROMISE).resolve("User created.");
Restate.run("activate", () -> activateUser(userId));
Restate.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
@Shared
public String waitForUserCreation() {
return Restate.promise(USER_CREATED_PROMISE).future().await();
}
}
```
```go events.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/events.go?collapse_prequel"} theme={null}
type SignupWithEventsWorkflow struct{}
func (SignupWithEventsWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil || !success {
err = restate.Promise[string](ctx, "user-created").Reject(fmt.Errorf("creation failed"))
return false, err
}
if err := restate.Promise[string](ctx, "user-created").Resolve("User created."); err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
return true, nil
}
func (SignupWithEventsWorkflow) WaitForUserCreation(ctx restate.WorkflowSharedContext) (string, error) {
return restate.Promise[string](ctx, "user-created").Result()
}
```
```python signup_with_events.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_events.py?collapse_prequel"} theme={null}
signup_with_events = restate.Workflow("SignupWithEventsWorkflow")
@signup_with_events.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
await ctx.promise("user-created").reject("Creation failed.")
return False
await ctx.promise("user-created", type_hint=str).resolve("User created.")
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return True
@signup_with_events.handler("waitForUserCreation")
async def wait_for_user_creation(ctx: WorkflowSharedContext) -> str:
return await ctx.promise("user-created").value()
```
Here, external clients can wait for the user to be created in the database.
These handlers can be called up to the workflow's retention period ([default one day](/services/configuration#workflow-retention).
Submit the workflow asynchronously with the `send` verb:
```bash theme={null}
curl localhost:8080/restate/send/SignupWithEventsWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithEventsWorkflow/johndoe/waitForUserCreation
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithEventsWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithEventsWorkflow/johndoe/waitForUserCreation
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithEventsWorkflow/johndoe/Run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithEventsWorkflow/johndoe/WaitForUserCreation
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithEventsWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithEventsWorkflow/johndoe/waitForUserCreation
```
You will get a response like `"User created."`. If the promise has not been resolved yet, the request waits until it is.
### Timers and Scheduling
Use durable timers for long-running workflows with timeouts and retries:
```ts signup-with-timers.ts expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-timers.ts?collapse_prequel"} theme={null}
export const signupWithTimers = restate.workflow({
name: "SignupWithTimersWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
const secret = ctx.rand.uuidv4();
await ctx.run("verify", () =>
sendVerificationEmail(userId, user, secret),
);
const clickedPromise = ctx.promise("email-verified").get();
const verificationTimeout = ctx.sleep({ days: 1 });
while (true) {
const reminderTimer = ctx.sleep({ seconds: 15 });
// Wait for email verification, reminder timer or timeout
const result = await RestatePromise.race([
clickedPromise.map(() => "verified"),
reminderTimer.map(() => "reminder"),
verificationTimeout.map(() => "timeout"),
]);
switch (result) {
case "verified":
const clickedSecret = await clickedPromise;
return { success: clickedSecret === secret };
case "reminder":
await ctx.run("send reminder", () =>
sendReminderEmail(userId, user, secret),
);
break;
case "timeout":
throw new TerminalError(
"Email verification timed out after 24 hours",
);
}
}
},
verifyEmail: async (
ctx: WorkflowSharedContext,
req: { secret: string },
) => {
await ctx.promise("email-verified").resolve(req.secret);
},
},
});
```
```java SignupWithTimersWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithTimersWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithTimersWorkflow {
private static final DurablePromiseKey EMAIL_VERIFIED_PROMISE =
DurablePromiseKey.of("email-verified", String.class);
@Workflow
public boolean run(User user) {
String userId = Restate.key();
var confirmationFuture = Restate.promise(EMAIL_VERIFIED_PROMISE).future();
var secret = Restate.random().nextUUID().toString();
Restate.run("verify", () -> sendVerificationEmail(userId, user, secret));
var verificationTimeout = Restate.timer(Duration.ofDays(1));
while (true) {
var reminderTimer = Restate.timer(Duration.ofSeconds(10));
var selected =
Select.select()
.when(confirmationFuture, res -> "verified")
.when(reminderTimer, unused -> "reminder")
.when(verificationTimeout, unused -> "timeout")
.await();
switch (selected) {
case "verified":
var clickedSecret = confirmationFuture.await();
return secret.equals(clickedSecret);
case "reminder":
Restate.run("send reminder", () -> sendReminderEmail(userId, user, secret));
break;
case "timeout":
throw new TerminalException("Verification timed out");
}
}
}
@Shared
public void verifyEmail(VerifyEmailRequest req) {
Restate.promiseHandle(EMAIL_VERIFIED_PROMISE).resolve(req.secret());
}
}
```
```go timers.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/timers.go?collapse_prequel"} theme={null}
type SignupWithTimersWorkflow struct{}
func (SignupWithTimersWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
secret := restate.UUID(ctx).String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendVerificationEmail(userID, user, secret)
}, restate.WithName("verify"))
if err != nil {
return false, err
}
clickedPromise := restate.Promise[string](ctx, "email-verified")
verificationTimeoutFuture := restate.After(ctx, 24*time.Hour)
for {
reminderTimerFuture := restate.After(ctx, 15*time.Second)
// Create futures for racing
resFut, err := restate.WaitFirst(ctx,
clickedPromise,
reminderTimerFuture,
verificationTimeoutFuture,
)
if err != nil {
return false, err
}
switch resFut {
case clickedPromise:
clickedSecret, err := clickedPromise.Result()
if err != nil {
return false, err
}
return clickedSecret == secret, nil
case reminderTimerFuture:
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendReminderEmail(userID, user, secret)
}, restate.WithName("send reminder"))
if err != nil {
return false, err
}
break // Break out the switch to continue the main loop
case verificationTimeoutFuture:
return false, restate.TerminalErrorf("email verification timed out after 24 hours")
}
}
}
func (SignupWithTimersWorkflow) VerifyEmail(ctx restate.WorkflowSharedContext, req VerifyEmailRequest) error {
return restate.Promise[string](ctx, "email-verified").Resolve(req.Secret)
}
```
```python signup_with_timers.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_timers.py?collapse_prequel"} theme={null}
signup_with_timers = restate.Workflow("SignupWithTimersWorkflow")
@signup_with_timers.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
secret = str(ctx.uuid())
await ctx.run_typed(
"verify",
send_verification_email,
user_id=user_id,
user=user,
verification_secret=secret,
)
clicked_promise = ctx.promise("email-verified", type_hint=str)
verification_timeout = ctx.sleep(timedelta(days=1))
while True:
reminder_timer = ctx.sleep(timedelta(seconds=15))
# Wait for email verification, reminder timer or timeout
result = await restate.select(
verification=clicked_promise.value(),
reminder=reminder_timer,
timeout=verification_timeout,
)
match result:
case ["verification", clicked_secret]:
return clicked_secret == secret
case ["reminder", _]:
await ctx.run_typed(
"remind",
send_reminder_email,
user_id=user_id,
user=user,
verification_secret=secret,
)
case ["timeout", _]:
raise TerminalError("Email verification timed out after 24 hours")
@signup_with_timers.handler("verifyEmail")
async def verify_email(ctx: WorkflowSharedContext, req: VerifyEmailRequest) -> None:
await ctx.promise("email-verified", type_hint=str).resolve(req.secret)
```
Because Restate lets you write workflows as regular functions, you can use your language's native constructs like `while`/`for` loops, `if` statements, and `switch` cases to control flow.
This makes it easy to implement complex logic with timers, loops, and conditional execution.
Submit the workflow asynchronously with the `send` verb:
```bash theme={null}
curl localhost:8080/restate/send/SignupWithTimersWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithTimersWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithTimersWorkflow/johndoe/Run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithTimersWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
See in the UI how the workflow is waiting for the email verification to be resolved, and sends reminder emails every 15 seconds:
Try killing the service and restarting it. The workflow will continue sending reminders as if it never stopped.
To resolve the promise, **copy over the curl request from the service logs**, which looks like this:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
Now the UI will show the workflow completed successfully.
### Parallel Execution
The timers example ran three operations in parallel: two timers and awaiting a promise.
Restate supports different ways of waiting for parallel operations to complete and takes care of retries and recovery for you.
Have a look at the Concurrent Tasks docs for your SDK to learn more ([TS](/develop/ts/concurrent-tasks) / [Java / Kotlin](/develop/java/concurrent-tasks) / [Python](/develop/python/concurrent-tasks) / [Go](/develop/go/concurrent-tasks)).
## Error Handling
By default, Restate retries failures infinitely with an exponential backoff strategy.
For some failures, you might not want to retry or only retry a limited number of times.
For these cases, Restate distinguishes between two types of errors: transient errors and terminal errors.
### Transient vs Terminal Errors
* **Transient errors**: These are temporary issues that can be retried, such as network timeouts or service unavailability. Restate automatically retries these errors.
* **Terminal errors**: These indicate a failure that will not be retried, such as invalid input or business logic violations. Restate stops execution and allows you to handle these errors gracefully.
Throw a terminal error in your handler to indicate a terminal failure:
```typescript {"CODE_LOAD::ts/src/tour/workflows/terminal_error.ts#terminal_error"} theme={null}
throw new TerminalError("Subscription plan not available");
```
```java {"CODE_LOAD::java/src/main/java/tour/workflows/WorkflowErrorHandler.java#here"} theme={null}
throw new TerminalException("Subscription plan not available");
```
```go {"CODE_LOAD::go/tour/workflows/errorhandling.go#here"} theme={null}
return restate.ToTerminalError(fmt.Errorf("subscription plan not available"))
```
```python {"CODE_LOAD::python/src/tour/workflows/terminal_error.py#here"} theme={null}
from restate.exceptions import TerminalError
raise TerminalError("Invalid credit card")
```
### Configuring Retry Behavior
You can limit the number of retries of a run block:
```ts signup-with-retries.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-retries.ts#retries"} theme={null}
try {
const retryPolicy = {
maxRetryAttempts: 3,
initialRetryInterval: { seconds: 1 },
};
await ctx.run("welcome", () => sendWelcomeEmail(user), retryPolicy);
} catch (error) {
// This gets hit on retry exhaustion with a terminal error
// Log and continue; without letting the workflow fail
console.error("Failed to send welcome email after retries:", error);
}
```
```java SignupWithRetriesWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithRetriesWorkflow.java#retries"} theme={null}
try {
RetryPolicy myRunRetryPolicy =
RetryPolicy.defaultPolicy()
.setInitialDelay(Duration.ofMillis(500))
.setExponentiationFactor(2)
.setMaxDelay(Duration.ofSeconds(10))
.setMaxAttempts(3)
.setMaxDuration(Duration.ofSeconds(30));
Restate.run("welcome", myRunRetryPolicy, () -> sendWelcomeEmail(user));
} catch (TerminalException error) {
// This gets hit on retry exhaustion with a terminal error
// Log and continue; without letting the workflow fail
System.err.println("Failed to send welcome email after retries: " + error.getMessage());
}
```
```go retries.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/retries.go#retries"} theme={null}
_, err = restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
},
restate.WithName("welcome"),
restate.WithMaxRetryAttempts(3),
restate.WithInitialRetryInterval(1000),
)
if err != nil {
// This gets hit on retry exhaustion with a terminal error
// Log and continue; without letting the workflow fail
fmt.Printf("Couldn't send the email due to terminal error %s", err)
}
```
```python signup_with_retries.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_retries.py?collapse_prequel"} theme={null}
signup_with_retries = restate.Workflow("SignupWithRetriesWorkflow")
@signup_with_retries.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
return False
await ctx.run_typed("activate", activate_user, user_id=user_id)
# Configure retry policy
try:
await ctx.run_typed(
"welcome",
send_welcome_email,
restate.RunOptions(
max_attempts=3, max_retry_duration=timedelta(seconds=30)
),
user=user,
)
except TerminalError as error:
# This gets hit on retry exhaustion with a terminal error
# Log and continue; without letting the workflow fail
print(f"Failed to send welcome email after retries: {error}")
return True
```
When the retries are exhausted, the run block will throw a terminal error, that you can handle in your handler logic.
Submit the workflow for Alice:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithRetriesWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithRetriesWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithRetriesWorkflow/alice/Run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithRetriesWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
In the UI, you can see how the workflow is waiting for the `welcome` step to finish, and how it retries sending the welcome email up to 3 times across three seconds.
After three attempts, the workflow continues without failing:
Learn more with the [Error Handling Guide](/guides/error-handling).
## Sagas and rollback
On a terminal failure, Restate stops the execution of the handler.
You might, however, want to roll back the changes made by the workflow to keep your system in a consistent state.
This is where Sagas come in.
Sagas are a pattern for rolling back changes made by a workflow when it fails.
In Restate, you can implement a saga by building a list of compensating actions for each step of the workflow.
On a terminal failure, you execute them in reverse order:
```ts signup-with-sagas.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-sagas.ts?collapse_prequel"} theme={null}
export const signupWithSagas = restate.workflow({
name: "SignupWithSagasWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
const compensations = [];
try {
compensations.push(() => ctx.run("delete", () => deleteUser(userId)));
await ctx.run("create", () => createUser(userId, user));
compensations.push(() =>
ctx.run("deactivate", () => deactivateUser(userId)),
);
await ctx
.run("activate", () => activateUser(userId))
.orTimeout({ minutes: 5 });
compensations.push(() =>
ctx.run("unsubscribe", () => cancelSubscription(user)),
);
await ctx.run("subscribe", () => subscribeToPaidPlan(user));
} catch (e) {
if (e instanceof restate.TerminalError) {
for (const compensation of compensations.reverse()) {
await compensation();
}
}
return { success: false };
}
return { success: true };
},
},
});
```
```java SignupWithSagasWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithSagasWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithSagasWorkflow {
@Workflow
public boolean run(User user) {
String userId = Restate.key();
List compensations = new ArrayList<>();
try {
compensations.add(() -> Restate.run("delete", () -> deleteUser(userId)));
Restate.run("create", () -> createUser(userId, user));
compensations.add(() -> Restate.run("deactivate", () -> deactivateUser(userId)));
Restate.run("activate", () -> activateUser(userId));
compensations.add(() -> Restate.run("unsubscribe", () -> cancelSubscription(user)));
Restate.run("subscribe", () -> subscribeToPaidPlan(user));
} catch (TerminalException e) {
// Run compensations in reverse order
Collections.reverse(compensations);
for (Runnable compensation : compensations) {
compensation.run();
}
return false;
}
return true;
}
}
```
```go sagas.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/sagas.go?collapse_prequel"} theme={null}
type SagasWorkflow struct{}
func (SagasWorkflow) Run(ctx restate.WorkflowContext, user User) (res bool, err error) {
userID := restate.Key(ctx)
var compensations []func() error
defer func() {
// All errors that end up here are terminal errors, so run compensations
// (Retry-able errors got returned by the SDK without ending up here)
if err != nil {
for _, compensation := range slices.Backward(compensations) {
if compErr := compensation(); compErr != nil {
err = compErr
}
}
}
}()
// Add compensation for user creation
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return DeleteUser(userID)
})
return err
})
_, err = restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil {
return false, err
}
// Add compensation for user activation
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return DeactivateUser(userID)
})
return err
})
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
})
if err != nil {
return false, err
}
// Add compensation for subscription
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return CancelSubscription(user)
})
return err
})
_, err = restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return SubscribeToPaidPlan(user)
})
if err != nil {
return false, err
}
return true, nil
}
```
```python signup_with_sagas.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_sagas.py?collapse_prequel"} theme={null}
signup_with_sagas = restate.Workflow("SignupWithSagasWorkflow")
@signup_with_sagas.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
compensations = []
try:
compensations.append(
lambda: ctx.run_typed("delete", delete_user, user_id=user_id)
)
await ctx.run_typed("create", create_user, user_id=user_id, user=user)
compensations.append(
lambda: ctx.run_typed("deactivate", deactivate_user, user_id=user_id)
)
await ctx.run_typed("activate", activate_user, user_id=user_id)
compensations.append(
lambda: ctx.run_typed("unsubscribe", cancel_subscription, user=user)
)
await ctx.run_typed("subscribe", subscribe_to_paid_plan, user=user)
except TerminalError:
# Run compensations in reverse order
for compensation in reversed(compensations):
await compensation()
return False
return True
```
**Benefits with Restate:**
* The list of compensations can be recovered after a crash, and Restate knows which compensations still need to be run.
* Sagas always run till completion (success or complete rollback)
* Full trace of all operations and compensations
* No complex state machines needed
Submit the workflow for Alice:
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSagasWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSagasWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSagasWorkflow/alice/Run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/call/SignupWithSagasWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
Alice is not able to get a subscription, so the workflow will fail and run compensations:
Learn more with the [Sagas Guide](/guides/sagas).
## Cancellation
You can cancel user signup workflows via HTTP, CLI, UI, or programmatically from other services.
When you cancel a workflow, Restate stops the execution by throwing a Terminal Error.
This allows your handler to run compensating actions or clean up resources.
First, the cancellation gets propagated to the leaf nodes of the call tree (in case the workflow called other services or workflows).
Then, the cancellation propagates back up the tree, allowing each handler to run its compensations.
Start the workflow asynchronously with the `send` verb:
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/eve/run \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/eve/run \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/eve/Run \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
```bash theme={null}
curl localhost:8080/restate/send/SignupWithSignalsWorkflow/eve/run \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
This returns the invocation ID, which you can use to cancel the workflow later via the UI, CLI or HTTP:
```bash CLI theme={null}
restate invocations cancel inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz
```
```bash curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/cancel
```
You can see the cancellation in the UI:
Check out the SDK docs, to learn how to programmatically cancel a workflow from another service ([TS](/develop/ts/service-communication#cancel-an-invocation) / [Java / Kotlin](/develop/java/service-communication#cancel-an-invocation) / [Python](/develop/python/service-communication#cancel-an-invocation)/ [Go](/develop/go/service-communication#cancel-an-invocation)).
## Serverless Deployment
Restate lets you run your workflows and services on serverless platforms like AWS Lambda or Google Cloud Run.
Restate automatically suspends workflows when they are waiting for events or timers, and resumes them when the event occurs or the timer expires.
This means you can run long-running workflows on function-as-a-service platforms without paying for the wait time.
Turning your signup workflow into a serverless function is as simple as adapting the endpoint:
```typescript {"CODE_LOAD::ts/src/tour/workflows/serving_lambda.ts#lambda"} theme={null}
import * as restate from "@restatedev/restate-sdk/lambda";
export const handler = restate.createEndpointHandler({
services: [signupWorkflow],
});
```
Learn more from the [serving docs](/develop/ts/serving).
```java {"CODE_LOAD::java/src/main/java/tour/workflows/WorkflowServingLambda.java#here"} theme={null}
import dev.restate.sdk.endpoint.Endpoint;
import dev.restate.sdk.lambda.BaseRestateLambdaHandler;
class MyLambdaHandler extends BaseRestateLambdaHandler {
@Override
public void register(Endpoint.Builder builder) {
builder.bind(new SignupWorkflow());
}
}
```
Learn more from the [serving docs](/develop/java/serving).
```go {"CODE_LOAD::go/tour/workflows/servinglambda.go#here"} theme={null}
handler, err := server.NewRestate().
Bind(restate.Reflect(SignupWorkflow{})).
Bidirectional(false).
LambdaHandler()
if err != nil {
log.Fatal(err)
}
lambda.Start(handler)
```
Learn more from the [serving docs](/develop/go/serving).
```python {"CODE_LOAD::python/src/tour/workflows/lambda_handler.py#here"} theme={null}
handler = restate.app(services=[signup_workflow])
```
Learn more from the [serving docs](/develop/python/serving).
## Summary
Restate workflows provide:
* **Natural Programming**: Write workflows as regular functions in your preferred language
* **Automatic Durability**: Built-in resilience without infrastructure complexity
* **Flexible Patterns**: State management, events, timers, and parallel execution
* **Modern Deployment**: Strong serverless support and simple single-binary deployment
With Restate, you can build complex, long-running workflows using familiar programming patterns while getting the durability you need.
# AI Agents
Source: https://docs.restate.dev/use-cases/ai-agents
Build resilient, observable AI agents that recover from failures and handle complex multi-step tasks.
## Durable Agents and Workflows
Restate automatically handles the reliability challenges of AI agents:
* **Automatically retry transient errors** like rate limits and network failures
* **Persist steps** (LLM calls, tools) and recover previous progress after failures
* **Suspend long-running agents** when idle to save costs
## Plugs into Popular SDKs
Restate works independently of any SDK and specific AI stack, but its lightweight programming abstraction integrates easily into many popular SDKs. A few lines turn your agent into a durable agent.
```typescript {"CODE_LOAD::ts/src/usecases/agents/weather-agent.ts#here"} theme={null}
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(restateContext, { maxRetryAttempts: 3 }),
});
```
Works with [Vercel AI SDK](/ai/sdk-integrations/vercel-ai-sdk), [OpenAI](/ai/sdk-integrations/openai-agents-sdk), and [others](/ai#llm-&-agent-sdk-integrations).
## Human-in-the-Loop and Workflow Patterns
Restate's workflows-as-code and building blocks make it easy to reliably implement:
Durable waiting for human decisions with crash-proof timeouts
Speed up multi-step workflows with recoverable parallel tasks
Break complex agents into smaller, specialized workflows
Coordinate specialized agents with reliable communication
Automatically undo previous actions when later steps fail
Build agents that can be paused, modified, and resumed during execution
## Observability and Debugging
See all ongoing executions with detailed journals of agent steps:
* **Complete execution timeline**: Every LLM call and tool execution
* **Debug failed agents**: Inspect exactly where and why agents failed
* **Agent control**: Pause, resume, restart agents during development and production
## End-to-End Resilient Applications
Agents are just a part of your application. Restate covers the plumbing around your agents:
* **Queuing, state, session management**: Built-in primitives for reliable agent coordination
* **Cost and concurrency control**: Cap how many agent invocations run at once with [flow control](/services/flow-control), putting a ceiling on LLM and API spend
* **Deterministic workflows**: Complement agents with structured business logic
* **Reliable asynchronous tasks**: Handle background work and inter-service communication
## Flexible Deployments and Scalability
Restate's durable execution runtime lets you run your durable code where you want at the scale you want:
* **Scale to millions** of concurrent agent executions
* **Deploy your agents** on FaaS or containers
* **You own the infrastructure**: Run on Restate Cloud or self-host
## Getting Started
Set up Restate and run your first agent
Build durable agents, chatbots, and multi-agent systems
Explore templates, examples, and SDK integrations
Questions? Join our community on [Discord](https://discord.restate.dev) or [Slack](https://slack.restate.dev).
# Event Processing
Source: https://docs.restate.dev/use-cases/event-processing
Build lightweight, transactional event handlers with built-in resiliency.
Kafka event processing requires managing Kafka consumers, handling retries, maintaining state stores, and coordinating complex workflows. Restate eliminates this complexity by providing **lightweight, transactional event processing** with zero consumer management and built-in state.
## Workflows from Kafka
Build event handlers with complex control flow, loops, timers, and transactional guarantees:
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/eventprocessing/user-feed.ts#here"} theme={null}
export default restate.object({
name: "userFeed",
handlers: {
processPost: async (ctx: restate.ObjectContext, post: SocialMediaPost) => {
const userId = ctx.key;
// Durable side effect: persisted and replayed on retries
const postId = await ctx.run(() => createPost(userId, post));
// Wait for processing to complete with durable timers
while ((await ctx.run(() => getPostStatus(postId))) === PENDING) {
await ctx.sleep({ seconds: 5 });
}
await ctx.run(() => updateUserFeed(userId, postId));
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/eventprocessing/eventtransactions/UserFeed.java#here"} theme={null}
@VirtualObject
public class UserFeed {
@Handler
public void processPost(SocialMediaPost post) {
String userId = Restate.key();
String postId = Restate.run("create-post", String.class, () -> createPost(userId, post));
while (Restate.run("get-post-status", String.class, () -> getPostStatus(postId))
.equals("PENDING")) {
Restate.sleep(Duration.ofSeconds(5));
}
Restate.run("update-user-feed", () -> updateUserFeed(userId, postId));
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/eventprocessing/user_feed.py#here"} theme={null}
user_feed = restate.VirtualObject("UserFeed")
@user_feed.handler()
async def process_post(ctx: restate.ObjectContext, post: SocialMediaPost):
user_id = ctx.key()
post_id = await ctx.run_typed(
"create post", create_post, user_id=user_id, post=post
)
while (
await ctx.run_typed("get status", get_post_status, post_id=post_id)
== Status.PENDING
):
await ctx.sleep(timedelta(seconds=5))
await ctx.run_typed("update feed", update_user_feed, user=user_id, post_id=post_id)
```
```go Go {"CODE_LOAD::go/usecases/eventprocessing/userfeed.go#here"} theme={null}
type UserFeed struct{}
func (UserFeed) ProcessPost(ctx restate.ObjectContext, post SocialMediaPost) error {
var userId = restate.Key(ctx)
postId, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreatePost(userId, post)
})
if err != nil {
return err
}
for {
status, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return GetPostStatus(postId), nil
})
if err != nil {
return err
}
if status != PENDING {
break
}
if err = restate.Sleep(ctx, 5*time.Second); err != nil {
//
return err
}
}
if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return UpdateUserFeed(userId, postId)
}); err != nil {
return err
}
return nil
}
```
**Key Benefits:**
* **Push-based delivery**: Events delivered directly to handlers with zero consumer management
* **Durable execution**: Failed handlers are retried with exponential backoff until they succeed. Handlers replay previously completed steps and resume exactly where they left off.
* **Parallel processing**: Events for different keys process concurrently, like a queue per object key
* **Timers and scheduling**: Timers and delays that survive crashes and restarts for **long-running workflows**
## Stateful Event Handlers
Maintain K/V state across events:
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/eventprocessing/delivery-tracker.ts#here"} theme={null}
export default restate.object({
name: "delivery-tracker",
handlers: {
register: async (ctx: restate.ObjectContext, delivery: Delivery) =>
ctx.set("delivery", delivery),
setLocation: async (ctx: restate.ObjectContext, location: Location) => {
const delivery = await ctx.get("delivery");
if (!delivery) {
throw new TerminalError(`Delivery not found`);
}
delivery.locations.push(location);
ctx.set("delivery", delivery);
},
getDelivery: shared(async (ctx: restate.ObjectSharedContext) =>
ctx.get("delivery")
),
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/eventprocessing/eventenrichment/DeliveryTracker.java#here"} theme={null}
@VirtualObject
public class DeliveryTracker {
private static final StateKey DELIVERY = StateKey.of("delivery", Delivery.class);
@Handler
public void register(Delivery packageInfo) {
Restate.state().set(DELIVERY, packageInfo);
}
@Handler
public void setLocation(Location location) {
var delivery =
Restate.state()
.get(DELIVERY)
.orElseThrow(() -> new TerminalException("Delivery not found"));
delivery.addLocation(location);
Restate.state().set(DELIVERY, delivery);
}
@Shared
public Delivery getDelivery() {
return Restate.state()
.get(DELIVERY)
.orElseThrow(() -> new TerminalException("Delivery not found"));
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/eventprocessing/delivery_tracker.py#here"} theme={null}
delivery_tracker = restate.VirtualObject("DeliveryTracker")
@delivery_tracker.handler()
async def register(ctx: restate.ObjectContext, delivery: Delivery):
ctx.set("delivery", delivery)
@delivery_tracker.handler()
async def set_location(ctx: restate.ObjectContext, location: Location):
delivery = await ctx.get("delivery", type_hint=Delivery)
if delivery is None:
raise TerminalError(f"Delivery {ctx.key()} not found")
delivery.locations.append(location)
ctx.set("delivery", delivery)
@delivery_tracker.handler(kind="shared")
async def get_delivery(ctx: restate.ObjectSharedContext) -> Delivery:
delivery = await ctx.get("delivery", type_hint=Delivery)
if delivery is None:
raise TerminalError(f"Delivery {ctx.key()} not found")
return delivery
```
```go Go {"CODE_LOAD::go/usecases/eventprocessing/packagetracker.go#here"} theme={null}
type DeliveryTracker struct{}
func (DeliveryTracker) Register(ctx restate.ObjectContext, delivery Delivery) error {
restate.Set[Delivery](ctx, "delivery", delivery)
return nil
}
func (DeliveryTracker) SetLocation(ctx restate.ObjectContext, location Location) error {
packageInfo, err := restate.Get[*Delivery](ctx, "delivery")
if err != nil {
return err
}
if packageInfo == nil {
return restate.ToTerminalError(errors.New("delivery not found"))
}
packageInfo.Locations = append(packageInfo.Locations, location)
restate.Set[Delivery](ctx, "delivery", *packageInfo)
return nil
}
func (DeliveryTracker) GetDelivery(ctx restate.ObjectSharedContext) (*Delivery, error) {
return restate.Get[*Delivery](ctx, "delivery")
}
```
**Key Benefits**:
* **Persistent state**: Store and retrieve state directly in handlers without external stores
* **Built-in consistency**: State operations are always consistent with execution
* **Agents, actors, digital twins**: Model stateful entities that react to events
## When to Choose Restate
**✅ Choose Restate when you need:**
* **Kafka integration**: Process Kafka events with zero consumer management
* **Reliable processing**: Automatic retry and recovery for failed event handlers
* **Transactional processing**: Execute side effects with durable execution guarantees
* **Stateful event processing**: Maintain state across events without external stores
* **Event-driven workflows**: Build complex flows with loops, timers, and conditions
Processing events with Restate? Join our community on [Discord](https://discord.restate.dev) or [Slack](https://slack.restate.dev) to discuss your use case.
## Comparison with Other Solutions
| Feature | Restate | Traditional Kafka Processing | Stream Processing Frameworks |
| ----------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **Event Delivery** | Push-based to durable handlers | Polling-based consumer groups | Built-in sources and sinks |
| **Consumer Management** | Zero configuration | Manual offset and consumer group management | Zero configuration |
| **Failure Recovery** | Fine-grained progress persistence with durable side effects | Coarse-grained offset commits with potential reprocessing | Coarse-grained checkpointing with potential duplicate side effects |
| **State Management** | Built-in durable state | External state store required | Built-in state store |
| **Queuing Semantics** | Queue-per-key with ordering guarantees | Queue-per-partition with ordering guarantees | Queue-per-partition (inherited from Kafka) |
| **Complex Workflows** | Unlimited control flow: loops, timers, conditions | Long-running logic blocks consumer loop; requires external services | DAG-based processing with limited control flow |
| **Best For** | Event-driven state machines, transactional processing, complex workflows | Simple ETL and message processing | High-throughput analytics, aggregations, joins |
## Getting Started
Ready to build event processing systems with Restate? Here are your next steps:
Set up Restate and process your first events
Follow the quickstart to implement your first durable event handler
Explore templates, patterns, and end-to-end applications
Evaluating Restate and missing a feature? Contact us on [Discord](https://discord.restate.dev) or [Slack](https://slack.restate.dev).
# Microservice Orchestration
Source: https://docs.restate.dev/use-cases/microservice-orchestration
Build resilient, distributed microservices with durable execution, sagas, and reliable service communication.
Restate provides **durable execution primitives** that make distributed systems resilient by default, without the operational overhead.
## Resilient Orchestration
Build microservices that automatically recover from failures without losing progress:
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/microservices/order-service.ts#here"} theme={null}
export const orderService = restate.service({
name: "OrderService",
handlers: {
process: async (ctx: restate.Context, order: Order) => {
// Each step is automatically durable and resumable
const paymentId = ctx.rand.uuidv4();
await ctx.run(() => chargePayment(order.creditCard, paymentId));
for (const item of order.items) {
await ctx.run(() => reserveInventory(item.id, item.quantity));
}
return { success: true, paymentId };
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/microservices/OrderService.java#here"} theme={null}
@Service
public class OrderService {
@Handler
public OrderResult process(Order order) {
// Each step is automatically durable and resumable
String paymentId = Restate.random().nextUUID().toString();
Restate.run("charge-payment", () -> chargePayment(order.creditCard, paymentId));
for (var item : order.items) {
Restate.run("reserve-inventory", () -> reserveInventory(item.id, item.quantity));
}
return new OrderResult(true, paymentId);
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/microservices/order_service.py#here"} theme={null}
order_service = restate.Service("OrderService")
@order_service.handler()
async def process(ctx: restate.Context, order: Order):
# Each step is automatically durable and resumable
payment_id = str(uuid.uuid4())
await ctx.run_typed(
"charge", charge_payment, credit_card=order.credit_card, payment_id=payment_id
)
for item in order.items:
await ctx.run_typed(
f"reserve_{item.id}", reserve, item_id=item.id, amount=item.amount
)
return {"success": True, "payment_id": payment_id}
```
```go Go {"CODE_LOAD::go/usecases/microservices/orderservice.go#here"} theme={null}
type OrderService struct{}
func (OrderService) Process(ctx restate.Context, order Order) (OrderResult, error) {
// Each step is automatically durable and resumable
paymentID := restate.UUID(ctx).String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ChargePayment(order.CreditCard, paymentID)
})
if err != nil {
return OrderResult{}, err
}
for _, item := range order.Items {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ReserveInventory(item.ID, item.Quantity)
})
if err != nil {
return OrderResult{}, err
}
}
return OrderResult{Success: true, PaymentID: paymentID}, nil
}
```
* **Automatic recovery**: Code resumes exactly where it left off after failures
* **Standard development**: Write services like regular HTTP APIs
* **[Resilient Sagas](/guides/sagas)**: Implement complex multi-step transactions with resilient rollback
## Reliable Communication & Idempotency
Flexible communication patterns with strong delivery guarantees:
* **Zero message loss**: All service communication is durably logged
* **Built-in retries**: Automatic exponential backoff for transient failures
* **Scheduling**: Delay messages for future processing
* **Request deduplication**: Idempotency keys prevent duplicate processing
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/microservices/service-actions.ts#communication"} theme={null}
// Request-response: Wait for result
const payRef = await ctx.serviceClient(paymentService).charge(req);
// Fire-and-forget: Guaranteed delivery without waiting
ctx.serviceSendClient(emailService).emailTicket(req);
// Delayed execution: Schedule for later
ctx
.serviceSendClient(emailService)
.sendReminder(order, sendOpts({ delay: dayBefore(req.concertDate) }));
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/microservices/ServiceActions.java#communication"} theme={null}
// Request-response: Wait for result
var result = Restate.service(InventoryService.class).checkStock(item);
// Fire-and-forget: Guaranteed delivery without waiting
Restate.serviceHandle(EmailService.class).send(EmailService::emailTicket, order);
// Delayed execution: Schedule for later
Restate.serviceHandle(EmailService.class)
.send(EmailService::sendReminder, order, Duration.ofDays(21));
```
```python Python {"CODE_LOAD::python/src/usecases/microservices/service_actions.py#communication"} theme={null}
# Request-response: Wait for result
result = await ctx.service_call(charge_payment, req)
# Fire-and-forget: Guaranteed delivery without waiting
ctx.service_send(send_ticket_email, ticket)
# Delayed execution: Schedule for later
ctx.service_send(send_reminder, ticket, send_delay=timedelta(days=7))
```
```go Go {"CODE_LOAD::go/usecases/microservices/serviceactions.go#communication"} theme={null}
// Request-response: Wait for result
result, err := restate.Service[StockResult](ctx, "PaymentService", "Charge").Request(req)
if err != nil {
return err
}
_ = result
// Fire-and-forget: Guaranteed delivery without waiting
restate.ServiceSend(ctx, "EmailService", "EmailTicket").Send(ticket)
// Delayed execution: Schedule for later
restate.ServiceSend(ctx, "EmailService", "SendReminder").Send(ticket, restate.WithDelay(7*24*time.Hour))
```
## Durable Stateful Entities
Manage stateful entities without external databases or complex consistency mechanisms:
* **Durable persistence**: Application state survives crashes and deployments
* **Simple concurrency model**: Single-writer semantics prevent consistency issues and race conditions
* **Horizontal scaling**: Each object has its own message queue. Different entity keys process independently
* **Built-in querying**: Access state via UI and APIs
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/microservices/user-account.ts#here"} theme={null}
export default restate.object({
name: "UserAccount",
handlers: {
updateBalance: async (ctx: restate.ObjectContext, amount: number) => {
const balance = (await ctx.get("balance")) ?? 0;
const newBalance = balance + amount;
if (newBalance < 0) {
throw new TerminalError("Insufficient funds");
}
ctx.set("balance", newBalance);
return newBalance;
},
getBalance: shared(async (ctx: restate.ObjectSharedContext) => {
return (await ctx.get("balance")) ?? 0;
}),
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/microservices/UserAccount.java#here"} theme={null}
@VirtualObject
public class UserAccount {
private static final StateKey BALANCE = StateKey.of("balance", Double.class);
@Handler
public double updateBalance(double amount) {
double balance = Restate.state().get(BALANCE).orElse(0.0);
double newBalance = balance + amount;
if (newBalance < 0) {
throw new TerminalException("Insufficient funds");
}
Restate.state().set(BALANCE, newBalance);
return newBalance;
}
@Shared
public double getBalance() {
return Restate.state().get(BALANCE).orElse(0.0);
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/microservices/user_account.py#here"} theme={null}
user_account = restate.VirtualObject("UserAccount")
@user_account.handler()
async def update_balance(ctx: restate.ObjectContext, amount: float):
balance = await ctx.get("balance", type_hint=float) or 0.0
new_balance = balance + amount
if new_balance < 0.0:
raise TerminalError("Insufficient funds")
ctx.set("balance", new_balance)
return new_balance
@user_account.handler(kind="shared")
async def get_balance(ctx: restate.ObjectSharedContext):
return await ctx.get("balance", type_hint=float) or 0.0
```
```go Go {"CODE_LOAD::go/usecases/microservices/useraccount.go#here"} theme={null}
type UserAccount struct{}
func (UserAccount) UpdateBalance(ctx restate.ObjectContext, amount float64) (float64, error) {
balance, err := restate.Get[float64](ctx, "balance")
if err != nil {
return 0.0, err
}
newBalance := balance + amount
if newBalance < 0.0 {
return 0.0, restate.ToTerminalError(errors.New("insufficient funds"))
}
restate.Set(ctx, "balance", newBalance)
return newBalance, nil
}
func (UserAccount) GetBalance(ctx restate.ObjectSharedContext) (float64, error) {
return restate.Get[float64](ctx, "balance")
}
```
## Operational simplicity
Reduce infrastructure complexity (no need for queues + state stores + schedulers, etc.). A single binary including everything you need.
## Key Orchestration Patterns
Implement resilient rollback logic for non-transient failures
Use Durable Execution to make database operations resilient and consistent
Execute independent operations concurrently while maintaining durability
Call other services with guaranteed delivery, retries, and deduplication
Wait for external events and webhooks with promises that survive crashes
Implement consistent state machines that survive crashes and restarts
## Comparison with Other Solutions
| Feature | Restate | Traditional Orchestration |
| -------------------------- | ------------------------------- | ------------------------------------------------- |
| **Infrastructure** | Single binary deployment | Message brokers + workflow engines + state stores |
| **Service Communication** | Built-in reliable messaging | External message queues required |
| **State Management** | Integrated durable state | External state stores + locks |
| **Failure Recovery** | Automatic progress recovery | Manual checkpoint/restart logic |
| **Deployment Model** | Standard HTTP services | Standard HTTP services |
| **Development Experience** | Regular code + IDE support | Regular code + IDE support |
| **Observability** | Built-in UI & execution tracing | Manual setup |
## Getting Started
Ready to build resilient microservices with Restate? Here are your next steps:
Run your first Restate service
Learn orchestration patterns with interactive examples
Explore templates, patterns, and end-to-end applications
Evaluating Restate and missing a feature? Contact us on [Discord](https://discord.restate.dev) or [Slack](https://slack.restate.dev).
# Workflows
Source: https://docs.restate.dev/use-cases/workflows
Build resilient, low-latency workflows with code.
Restate lets you **write workflows as regular code** in your preferred programming language, with automatic resilience.
## Workflows as code
Write resilient workflows using familiar programming constructs:
* **Automatically retry transient errors** like infrastructure crashes and network failures
* Use **standard language constructs** (if/else, loops) and durable versions of familiar building blocks (e.g., timers, promises)
* **Handle errors naturally** with try/catch blocks and automatic retries
* **Test and debug** with your existing IDE and standard development tools
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/workflows/simple-signup.ts#here"} theme={null}
export const userSignup = restate.workflow({
name: "user-signup",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key; // unique workflow key
// Use regular if/else, loops, and functions
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) return { success };
// Execute durable steps
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success: true };
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/workflows/UserSignup.java#here"} theme={null}
@Workflow
public class UserSignup {
@Workflow
public boolean run(User user) {
String userId = Restate.key(); // unique workflow key
// Use regular if/else, loops, and functions
boolean success = Restate.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
return false;
}
// Execute durable steps
Restate.run("activate", () -> activateUser(userId));
Restate.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/workflows/signup.py#here"} theme={null}
user_signup = restate.Workflow("user-signup")
@user_signup.main()
async def run(ctx: restate.WorkflowContext, user: User) -> Dict[str, bool]:
# Unique workflow key
user_id = ctx.key()
# Use regular if/else, loops, and functions
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
return {"success": False}
# Execute durable steps
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return {"success": True}
```
```go Go {"CODE_LOAD::go/usecases/workflows/signup.go#here"} theme={null}
type UserSignup struct{}
func (UserSignup) Run(ctx restate.WorkflowContext, user User) (bool, error) {
// unique workflow key
userID := restate.Key(ctx)
// Use regular if/else, loops, and functions
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
})
if err != nil || !success {
return false, err
}
// Execute durable steps
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
})
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
})
if err != nil {
return false, err
}
return true, nil
}
```
## Low-Latency Workflows
Restate is built from the ground up for low-latency workflow execution. Restate workflows can be placed directly in the latency-sensitive path of user interactions:
* **Lightweight execution**: Workflows run like regular functions with minimal overhead
* **Event-driven foundation**: Built in Rust for high-performance, low-latency operations
* **No coordination delays**: Immediate workflow execution via a push-based model
[Benchmark results Restate v1.2](https://restate.dev/blog/building-a-modern-durable-execution-engine-from-first-principles/#some-performance-numbers)
## Simple Deployment Model
**Restate Server**: Restate is packaged as a single binary with built-in persistence and messaging. Run it as a single instance or in a high-availability cluster.
**Service Deployment**: Deploy your workflows using your existing deployment pipeline: containers, Kubernetes, serverless functions, or any HTTP-capable platform.
On FaaS, Restate suspends workflows while they are waiting (e.g. timer) to reduce costs.
## Key Workflow Patterns
Store workflow state that survives crashes and can be queried from external systems
Handle external events and signals without complex event sourcing infrastructure
Long-running processes with built-in timer management and timeout handling
Execute steps inline within the workflow or split them out into separate services
Speed up multi-step workflows with recoverable parallel tasks
Automatically undo previous actions when later steps fail
## Comparison with Other Solutions
| Feature | Restate | Traditional Orchestrators |
| ---------------------- | ------------------------------------------------ | ----------------------------------- |
| **Performance** | Low-latency, lightweight execution | High overhead, poll-for-work delays |
| **Language** | Native code (TS, Python, Go, Java, Kotlin, Rust) | DSLs or YAML |
| **Development** | Standard IDE, testing, debugging | Platform-specific tooling |
| **Infrastructure** | Single binary, no dependencies | Separate databases and queues |
| **Service Deployment** | Any platform (containers, serverless, K8s) | Worker-based deployment models |
| **State Management** | Built-in K/V state store | External state stores required |
## Getting Started
Ready to build workflows with Restate? Here are your next steps:
Run your first Restate service
Explore the APIs to build workflows with Restate
Explore templates, patterns, and end-to-end applications
Evaluating Restate and missing a feature? Contact us on [Discord](https://discord.restate.dev) or [Slack](https://slack.restate.dev).