Engineering research / distributed

Dapr 1.19: Stateful Workflow History, Cross-App Workflow Policies With requires, CRDs Before helm upgrade and Scheduler Placement

Dapr 1.19.0 shipped on 22 October 2026 with changes that affect how durable workflows are run and upgraded: workers that receive only new history, cross-app workflow operations gated by an access policy, a CRD step you must do before helm upgrade, and optional Scheduler-served actor placement.

By Sandeep Belgavi · Published 2026-10-25 · Prepared 2026-10-23

Recent development · Practical technical guide · 23 October 2026

Dapr 1.19.0 shipped on 22 October 2026 with changes that affect how durable workflows are run and upgraded: workers that receive only new history, cross-app workflow operations gated by an access policy, a CRD step you must do before helm upgrade, and optional Scheduler-served actor placement.

What shipped, and why it matters for durable workflows

Dapr 1.19.0 was released on 22 October 2026. For teams running Dapr Workflows, the durable-execution building block, several of its changes are not cosmetic: they change how a workflow worker exchanges history with the sidecar, add a security gate on operating another app workflow, and introduce an upgrade ordering step that, if skipped, silently drops a new security field. This article sticks to those workflow-facing changes and the upgrade mechanics, verified against the release notes, rather than the full changelog.

Workers now receive only the new history

Before 1.19, a workflow worker received the full event history of an instance on every turn. For a long-running instance that history grows without bound, and sending and parsing it dominated the time of each turn. In 1.19 a worker can advertise a stateful-history capability and keep each instance history in a local cache; the sidecar then sends only the new events of a turn, and the worker reconstructs the full history from its cache plus those events. On a reconnect, or if the worker has evicted the instance, the sidecar sends the full history again, so correctness does not depend on the cache. The Python, Java and .NET SDKs turn this on by default; the release notes give explicit switches to disable it per SDK.

SDKStateful historyDisable switch
PythonOn by defaultdisable_stateful_history=True on WorkflowRuntime
JavaOn by defaultWorkflowRuntimeBuilder.withStatefulHistoryDisabled(true)
.NETOn by defaultWorkflowRuntimeOptions.DisableStatefulHistory
GoOn by default from go-sdk v1.16.0client.WithStatefulHistoryDisabled() in durabletask-go
JavaScriptNot usedNew JS engine does not share the DurableTask client

A related change addresses idle streams: the sidecar now sends a health ping every 30 seconds to workers that advertise the capability, because load balancers such as AWS ALB and nginx close an idle stream after 60 seconds, and before 1.19 that cost a reconnect and a full history resend. The ping interval is configurable with DAPR_WORKFLOW_HEALTH_PING_INTERVAL.

Cross-app workflow operations, gated by a policy

In 1.19 a workflow client can operate on a workflow owned by another app in the same namespace: start, get, raise event, pause, resume, terminate, purge and rerun. The workflow request protos gained an optional app_id, and the HTTP endpoints accept an appID query parameter. What a caller may do is decided by the target app WorkflowAccessPolicy, per operation and per workflow name. The notable addition is a requires rule: an operation is allowed only when the caller propagated workflow history contains listed upstream events. The release notes give the example of a billing app allowing its charge activity only for callers whose history holds an event.raised named order.validated from the orders app, so a caller cannot skip the validation step.

Illustrative yaml, checked for syntax; adapt names and storage to your system.

# A WorkflowAccessPolicy protecting the billing app, allowing the orders app to
# schedule ChargeWF only if its propagated history proves order.validated ran.
apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: billing-policy
spec:
  scopes: ["billing"]          # the target app this policy is enforced for
  rules:
  - callers:
    - appID: orders            # the calling app
    workflows:
    - name: ChargeWF
      operations: ["schedule"]
      requires:                # ordered; max 20; only valid for a schedule-only rule
      - eventType: event.raised
        name: order.validated
        appID: orders

The requires block is an ordered list of up to 20 entries, each with an appID, an eventType drawn from a fixed set (activity.started, activity.completed, workflow.started, workflow.completed or event.raised) and a name; the rules themselves nest under callers and workflows or activities, and a policy with no rules denies every cross-app call. One caveat is load-bearing: the access-policy docs state that requires gates on the caller propagated history, which is only trustworthy when workflow history signing is enabled, so if signing is off a rule with a requires block can never grant access and fails closed. It applies to scheduling only. Note also that scheduling through an SDK client with an instance ID that already belongs to an active instance now fails with the gRPC AlreadyExists status, which lets a client use a fixed instance ID as an idempotent start.

The upgrade step that is easy to miss

Dapr 1.19 updates two CRDs: Configuration gains workflow latency-bucket fields, and WorkflowAccessPolicy gains the requires field. A helm upgrade does not update CRDs. The release notes are explicit about the consequence: until the 1.19 WorkflowAccessPolicy CRD is applied, the API server drops the unknown requires field, so a rule you wrote with requires is accepted and applied without that gate. In security terms, a policy you believe enforces an upstream-event requirement silently enforces nothing. The fix is to apply the 1.19 CRDs before the Helm upgrade, not after.

  1. Apply the 1.19 CRDs explicitly before upgrading the control plane.
  2. Upgrade the Dapr control plane and sidecars to 1.19.
  3. Only then rely on any WorkflowAccessPolicy that uses requires, and confirm the field survives a round-trip read of the applied resource.

Scheduler-served placement, and a worked upgrade picture

1.19 also lets the Scheduler place actors itself, sending each reminder straight to the host that runs the actor, as an opt-in that removes the need for a standalone Placement service. It is off by default and the standalone Placement service remains the default in 1.19. The release notes warn that enabling or disabling it restarts the Schedulers, so actor and workflow activity pauses while they come back up, and that every sidecar must be on 1.19 first. Picture a cluster upgrading a long-running order workflow: with stateful history on, each worker turn now ships only new events, so a workflow with thousands of history entries no longer re-sends them every step; the health ping keeps its stream alive behind an ALB; and once the CRDs are applied, a billing policy can require proof that validation ran. Each of these is a behaviour change to verify in staging, not a silent improvement.

When to wait

If you do not use Dapr Workflows or actors, most of this does not apply and the upgrade is routine. If you run workflows but have scripted around the full-history resend or the 60-second idle disconnect, upgrade deliberately: the new defaults change the shape of the traffic between worker and sidecar, and the release notes flag behaviour changes mostly in the SDKs. Do not enable Scheduler-served placement until every sidecar is on 1.19, and do not depend on a requires policy until the matching CRD is applied.

Failure modes

  • Running helm upgrade without applying the 1.19 CRDs first, so WorkflowAccessPolicy requires rules are dropped and enforce nothing.
  • Enabling Scheduler-served placement before all sidecars are on 1.19, so older sidecars that can only use the Placement service stall.
  • Assuming stateful history changes results; it changes only what is sent per turn, and a reconnect still sends the full history.
  • Leaving idle workflow streams behind a load balancer without the health-ping capability, keeping the pre-1.19 reconnect cost.
  • Reusing an active instance ID and treating the new AlreadyExists status as a generic error rather than an idempotent no-op.

Verification

In a staging cluster, apply the 1.19 CRDs, then read back an applied WorkflowAccessPolicy and assert the requires field is present; that round-trip is the direct test for the CRD-ordering trap. Start a long workflow and confirm, from worker logs or metrics, that turns after the first carry only new events, and that an idle stream survives past 60 seconds behind your load balancer. Schedule a workflow twice with the same instance ID and assert the second returns AlreadyExists. Treat Scheduler-served placement as a separate change: enable it only after every sidecar reports 1.19, and expect a short actor pause when you do.

Sources and scope

Checked against these sources on 23 October 2026. Event: Dapr 1.19.0 release notes, 22 October 2026; recheck it before acting. Examples and figures are illustrative, not measurements.

Apply the 1.19 CRDs before helm upgrade or a requires policy silently enforces nothing, expect stateful history and health pings to change worker-to-sidecar traffic, treat the AlreadyExists status as an idempotent start, and enable Scheduler-served placement only once every sidecar is on 1.19.