Sometimes your handlers need to pause and wait for external processes to complete. This is common in:
  • Human-in-the-loop workflows (approvals, reviews, manual steps)
  • External system integration (waiting for webhooks, async APIs)
  • AI agent patterns (tool execution, human oversight)
This pattern is also known as the callback or task token pattern.

Two Approaches

Restate provides two primitives for handling external events:
PrimitiveUse CaseKey Feature
AwakeablesServices & Virtual ObjectsUnique ID-based completion
Durable PromisesWorkflows onlyNamed promises for simpler signaling

How it works

Implementing this pattern in a distributed system is tricky, since you need to ensure that the handler can recover from failures and resume waiting for the external event. Restate makes promises are durable and distributed. They survive crashes and can be resolved or rejected by any handler in the workflow. To save costs on FaaS deployments, Restate lets the handler suspend while awaiting the promise, and invokes it again when the result is available.

Awakeables

Best for: Services and Virtual Objects where you need to coordinate with external systems.

Creating and waiting for awakeables

  1. Create an awakeable - Get a unique ID and promise
  2. Send the ID externally - Pass the awakeable ID to your external system
  3. Wait for result - Your handler suspends until the external system responds
awakeable := restate.Awakeable[string](ctx)
awakeableId := awakeable.Id()

if _, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
  return requestHumanReview(awakeableId)
}); err != nil {
  return err
}

review, err := awakeable.Result()
if err != nil {
  return err
}
Note that if you wait for an awakeable in an exclusive handler in a Virtual Object, all other calls to this object will be queued.

Resolving/rejecting Awakeables

External processes complete awakeables in two ways:
  • Resolve with success data → handler continues normally
  • Reject with error reason → throws a terminal error in the waiting handler

Via SDK (from other handlers)

Resolve:
restate.ResolveAwakeable(ctx, awakeableId, "Looks good!")
Reject:
restate.RejectAwakeable(ctx, awakeableId, fmt.Errorf("Cannot do review"))

Via HTTP API

External systems can complete awakeables using Restate’s HTTP API: Resolve with data:
curl localhost:8080/restate/awakeables/sign_1PePOqp/resolve \
  --json '"Looks good!"'
Reject with error:
curl localhost:8080/restate/awakeables/sign_1PePOqp/reject \
  -H 'content-type: text/plain' \
  -d 'Review rejected: insufficient documentation'

Durable Promises

Best for: Workflows where you need to signal between different workflow handlers. Key differences from awakeables:
  • No ID management - use logical names instead
  • Scoped to workflow execution lifetime
Use this for:
  • Sending data to the run handler
  • Have handlers wait for events emitted by the run handler
After a workflow’s run handler completes, other handlers can still be called for up to 24 hours (default). The results of resolved Durable Promises remain available during this time. Update the retention time via the service configuration.

Creating and waiting for promises

Wait for a promise by name:
review, err := restate.Promise[string](ctx, "review").Result()

Resolving/rejecting promises

Resolve/reject from any workflow handler:
err := restate.Promise[string](ctx, "review").Resolve(review)
if err != nil {
  return err
}

Complete workflow example

type ReviewWorkflow struct{}

func (ReviewWorkflow) Run(ctx restate.WorkflowContext, documentId string) (string, error) {
  // Send document for review
  if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
    return restate.Void{}, askReview(documentId)
  }); err != nil {
    return "", err
  }

  // Wait for external review submission
  review, err := restate.Promise[string](ctx, "review").Result()
  if err != nil {
    return "", err
  }

  // Process the review result
  return processReview(documentId, review)
}

func (ReviewWorkflow) SubmitReview(ctx restate.WorkflowSharedContext, review string) error {
  // Signal the waiting run handler
  return restate.Promise[string](ctx, "review").Resolve(review)
}

Two signaling patterns

External → Workflow (shown above): External handlers signal the run handler
  • Use for human approvals, external API responses, manual interventions
  • External handlers call the handler which resolves the promise
Workflow → External: Run handler signals other handlers waiting for workflow events
  • Use for step completion notifications, status updates, result broadcasting
  • Run handler resolves promises that external handlers are awaiting

Best Practices

  • Use awakeables for services/objects coordinating with external systems
  • Use durable promises for workflow signaling
  • Always handle rejections to gracefully manage failures
  • Include timeouts for long-running external processes