To call a Restate handler and wait for its result:
// To call a Service:const svcResponse = await ctx.serviceClient(myService).myHandler("Hi");// To call a Virtual Object:const objResponse = await ctx .objectClient(myObject, "Mary") .myHandler("Hi");// To call a Workflow:// `run` handler — can only be called once per workflow IDconst wfResponse = await ctx .workflowClient(myWorkflow, "wf-id") .run("Hi");// Other handlers can be called anytime within workflow retentionconst result = await ctx .workflowClient(myWorkflow, "wf-id") .interactWithWorkflow();
Use generic calls when you don’t have the service definition or need dynamic service names:
const response = await ctx.genericCall({ service: "MyObject", method: "myHandler", parameter: "Hi", key: "Mary", // drop this for Service calls inputSerde: restate.serde.json, outputSerde: restate.serde.json,});
Workflow retention
After a workflow’s run handler completes, other handlers can still be called for up to 24 hours (default).
Update this via the service configuration.
Request-response calls between exclusive handlers of Virtual Objects may lead to deadlocks:
Cross deadlock: A → B and B → A (same keys).
Cycle deadlock: A → B → C → A.
Use the UI or CLI to cancel and unblock deadlocked invocations.
To send a message to another Restate handler without waiting for a response:
// To message a Service:ctx.serviceSendClient(myService).myHandler("Hi");// To message a Virtual Object:ctx.objectSendClient(myObject, "Mary").myHandler("Hi");// To message a Workflow:// `run` handler — can only be called once per workflow IDctx.workflowSendClient(myWorkflow, "wf-id").run("Hi");// Other handlers can be called anytime within workflow retentionctx.workflowSendClient(myWorkflow, "wf-id").interactWithWorkflow();
Restate handles message delivery and retries, so the handler can complete and return without waiting for the message to be processed.Use generic send when you don’t have the service definition:
// To message a Service with a delay:ctx .serviceSendClient(myService) .myHandler("Hi", restate.rpc.sendOpts({ delay: { hours: 5 } }));// To message a Virtual Object with a delay:ctx .objectSendClient(myObject, "Mary") .myHandler("Hi", restate.rpc.sendOpts({ delay: { hours: 5 } }));// To message a Workflow with a delay:ctx .workflowSendClient(myWorkflow, "Mary") .run("Hi", restate.rpc.sendOpts({ delay: { hours: 5 } }));
Use generic send with a delay when you don’t have the service definition:
To prevent duplicate executions of the same call, add an idempotency key:
// For request-responseconst response = await ctx.serviceClient(myService).myHandler( "Hi", restate.rpc.opts({ idempotencyKey: "my-idempotency-key", }));// For sending a messagectx.serviceSendClient(myService).myHandler( "Hi", restate.rpc.sendOpts({ idempotencyKey: "my-idempotency-key", }));
Restate automatically deduplicates calls made during the same handler execution, so there’s no need to provide an idempotency key in that case.
However, if multiple handlers might call the same service independently, you can use an idempotency key to ensure deduplication across those calls.
When calling services, you need access to their type definitions for type safety. Here are four approaches to share service definitions without exposing implementation details:
Option 1: Export type-only service definition
Export only the service type information from where you define your service:
Create a TypeScript interface matching the service’s handler signatures. Useful when the service is in a different language or when you can’t import the type definition: