Policies
Reference for the declarative manifests that define Catalyst policies. For the conceptual story see Policies. For end-to-end usage see Resiliency and IDs.
Policies are applied with the diagrid CLI. The target project is provided by the --project flag, not by a field in the manifest:
diagrid resiliency create -f resiliency.yaml --project my-project
diagrid configuration create -f configuration.yaml --project my-project
diagrid workflow access-policy create -f workflow-access-policy.yaml --project my-project
Manifest envelope
Every policy manifest shares the same top-level shape:
apiVersion: dapr.io/v1alpha1
kind: <Resiliency | Configuration | WorkflowAccessPolicy>
metadata:
name: <policy-name>
scopes:
- <app-id-1>
- <app-id-2>
spec:
# ...kind-specific spec below
| Field | Type | Required | Description |
|---|---|---|---|
apiVersion | string | yes | Must be dapr.io/v1alpha1. |
kind | string | yes | Resiliency, Configuration, or WorkflowAccessPolicy. |
metadata.name | string | yes | Unique policy name within the project. |
scopes | string[] | no | Workloads the policy applies to. Top-level (sibling of spec). Omit to leave the policy unbound — except for WorkflowAccessPolicy, where omitting scopes applies the policy to every app in the project. |
Resiliency policy
A resiliency policy defines named retry, timeout, and circuit-breaker behaviours, then binds them to outbound calls by target.
Spec
spec:
policies:
timeouts:
<policy-name>: <duration>
retries:
<policy-name>:
policy: constant | exponential
duration: <duration>
maxInterval: <duration> # exponential only
maxRetries: <int> # -1 for unlimited
matching:
httpStatusCodes: "<comma-separated codes or ranges>"
gRPCStatusCodes: "<comma-separated codes>"
circuitBreakers:
<policy-name>:
maxRequests: <int>
interval: <duration>
timeout: <duration>
trip: "<CEL expression>"
targets:
apps:
<app-id>:
timeout: <timeout-policy-name>
retry: <retry-policy-name>
circuitBreaker: <circuit-breaker-policy-name>
circuitBreakerCacheSize: <int>
components:
<component-name>:
inbound:
timeout: <timeout-policy-name>
retry: <retry-policy-name>
circuitBreaker: <circuit-breaker-policy-name>
outbound:
timeout: <timeout-policy-name>
retry: <retry-policy-name>
circuitBreaker: <circuit-breaker-policy-name>
Retry policy fields
| Field | Type | Description |
|---|---|---|
policy | constant | exponential | Backoff shape. Constant waits duration between attempts; exponential grows the wait up to maxInterval. |
duration | duration string (e.g. 5s) | Wait between attempts (constant) or initial wait (exponential). |
maxInterval | duration string | Upper bound on the wait for exponential policies. Ignored for constant. |
maxRetries | int | Maximum attempts after the initial call. -1 means retry indefinitely. |
matching.httpStatusCodes | string | Comma-separated HTTP codes or ranges to retry, e.g. "500,501,502,503,504". Unmatched codes fail immediately. |
matching.gRPCStatusCodes | string | Comma-separated gRPC status codes to retry, e.g. "14,4,8". |
Circuit-breaker fields
| Field | Type | Description |
|---|---|---|
maxRequests | int | Number of probe requests permitted while the breaker is half-open. |
interval | duration string | Sliding window over which failures are counted. |
timeout | duration string | Time the breaker stays open before transitioning to half-open. |
trip | CEL expression | Trip condition. Evaluated against the variables below, e.g. "consecutiveFailures > 5". |
The trip expression is evaluated as CEL with the following variables in scope:
| Variable | Description |
|---|---|
requests | Total requests observed in the current window. |
consecutiveFailures | Consecutive failures since the last success. |
consecutiveSuccesses | Consecutive successes since the last failure. |
totalFailures | Total failures in the current window. |
totalSuccesses | Total successes in the current window. |
Target fields
apps.<app-id> binds policies to outbound service invocations to that app. components.<component-name>.outbound binds policies to calls out to the component; inbound binds policies to deliveries from the component (for example pub/sub message delivery into the app).
Each target slot accepts the name of a policy defined in policies.timeouts, policies.retries, or policies.circuitBreakers. Bindings are optional; omit a slot to leave that dimension unbounded for the target.
circuitBreakerCacheSize (apps only) caps the number of per-instance breakers cached for that target.
Defaults
If you omit a dimension, Catalyst applies these defaults at the data plane:
| Dimension | Default value |
|---|---|
| Timeout | 10s |
| Retry | constant, 5s between attempts, 5 retries, matching HTTP 500,501,502,503,504 and gRPC 14,4,8 |
| Circuit breaker | maxRequests: 1, interval: 5s, timeout: 20s, trip: consecutiveFailures > 5 |
Worked example
apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
name: order-service-resiliency
scopes:
- order-service
spec:
policies:
timeouts:
fastCall: 2s
retries:
transientRetry:
policy: exponential
duration: 200ms
maxInterval: 5s
maxRetries: 4
matching:
httpStatusCodes: "503,504"
gRPCStatusCodes: "14"
circuitBreakers:
defaultBreaker:
maxRequests: 1
interval: 30s
timeout: 60s
trip: "consecutiveFailures > 5"
targets:
apps:
inventory-service:
timeout: fastCall
retry: transientRetry
circuitBreaker: defaultBreaker
components:
orders-statestore:
outbound:
timeout: fastCall
retry: transientRetry
circuitBreaker: defaultBreaker
Configuration
A configuration sets app-level runtime behaviour: access control lists, HTTP pipelines, distributed tracing (private regions only), and workflow runtime parameters. For the access-control rules model and the CLI commands to manage them, see Secure service invocation with access control.
Spec
spec:
accessControl:
defaultAction: allow | deny
trustDomain: <trust-domain> # ignored: set by the platform
policies:
- appId: <caller-app-id>
defaultAction: allow | deny
trustDomain: <trust-domain>
namespace: <namespace> # ignored: set by the platform
operations:
- name: <operation-name>
httpVerb: ["GET", "POST", ...]
action: allow | deny
appHttpPipeline:
handlers:
- name: <middleware-name>
type: <middleware-type>
httpPipeline:
handlers:
- name: <middleware-name>
type: <middleware-type>
tracing: # private regions only
samplingRate: "<float-as-string>"
otel:
endpointAddress: <url>
isSecure: <bool>
protocol: <http | grpc>
timeout: <duration>
headers:
- name: <header-name>
value: <header-value>
- name: <header-name>
secretKeyRef:
name: <secret-name>
key: <field-in-secret>
workflow:
maxConcurrentWorkflowInvocations: <int> # private regions only
maxConcurrentActivityInvocations: <int> # private regions only
globalMaxConcurrentWorkflowInvocations: <int>
globalMaxConcurrentActivityInvocations: <int>
workflowConcurrencyLimits:
- name: <workflow-name>
maxConcurrent: <int>
activityConcurrencyLimits:
- name: <activity-name>
maxConcurrent: <int>
stateRetentionPolicy:
completed: <duration>
failed: <duration>
terminated: <duration>
anyTerminal: <duration>
Field reference
| Field | Notes |
|---|---|
accessControl | Service-invocation ACLs. The top-level defaultAction applies when no per-caller policy matches. Per-caller policies[] can refine to specific operations and HTTP verbs. defaultAction and action accept only allow or deny; httpVerb accepts only the nine standard HTTP methods; a policies[] entry must set appId and every operations[] entry must set name. |
accessControl.trustDomain | Top-level only. Set by the platform: a value you supply is accepted and then discarded, on create and on update alike. |
accessControl.policies[].trustDomain | Yours to set, and kept. Leave it out and it defaults to your organization's trust domain, which is what you want unless you are naming a caller from somewhere else. |
accessControl.policies[].namespace | Set by the platform. Whatever you supply is replaced with your project's namespace, prj-<project-namespace>, because that is the namespace the access-control decision is actually made against. |
appHttpPipeline | Middleware applied to traffic from the data plane to your application (inbound to the app). |
httpPipeline | Middleware applied to traffic from your application out through the data plane (outbound). |
tracing | Distributed tracing config. Only honored on projects in private regions. samplingRate is a string between "0" and "1". |
tracing.otel.headers | Headers added to every OTLP exporter request. Give each entry a name and then one of value (plaintext), secretKeyRef (a name/key pair naming a secret and the field in it), or envRef (an environment variable to read the value from). |
tracing.otel.timeout | Duration string bounding each OTLP exporter request. |
workflow.maxConcurrentWorkflowInvocationsworkflow.maxConcurrentActivityInvocations | Per-sidecar ceilings: the most a single Dapr instance schedules at once. Invocations beyond the ceiling queue rather than fail. Omit for no limit. Honored in private regions only — see the note below. |
workflow.globalMaxConcurrentWorkflowInvocationsworkflow.globalMaxConcurrentActivityInvocations | The same ceilings across every replica, enforced by the scheduler rather than per sidecar. Omit for no limit. |
workflow.workflowConcurrencyLimitsworkflow.activityConcurrencyLimits | Per-name ceilings, enforced across every replica by the scheduler. Each entry takes a name (the workflow or activity name to limit) and a maxConcurrent. Names must be non-empty and unique within a list, and maxConcurrent must be non-negative. |
workflow.stateRetentionPolicy | How long a workflow instance's state is kept after it reaches a terminal state, after which it is purged automatically. Set completed, failed or terminated per state, or anyTerminal for all of them; a specific state wins over anyTerminal. Values are duration strings such as 72h or 30m, including 0s to purge immediately. Omit the whole block and instances are never purged automatically. |
Some of this spec is accepted and then overwritten, with no error to tell you so. accessControl.trustDomain is always cleared and accessControl.policies[].namespace is always replaced, because the platform owns both. On a project in a public region, workflow.maxConcurrentWorkflowInvocations and workflow.maxConcurrentActivityInvocations are cleared too — read them back with diagrid configuration get and they are gone. Nothing else in workflow is affected: the global... ceilings, the per-name limits and stateRetentionPolicy are kept in every region. To bound per-sidecar concurrency in a public region, use the per-name limits instead.
Worked example
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: order-service-config
spec:
accessControl:
defaultAction: deny
policies:
- appId: api-gateway
defaultAction: deny
operations:
- name: /orders
httpVerb: ["POST"]
action: allow
workflow:
workflowConcurrencyLimits:
- name: OrderProcess
maxConcurrent: 20
activityConcurrencyLimits:
- name: ChargePayment
maxConcurrent: 5
stateRetentionPolicy:
completed: 24h
anyTerminal: 72h
The top-level accessControl.trustDomain and the per-caller namespace are left out because the platform sets both, and policies[].trustDomain is left out so it defaults to your organization's. The limits and the retention policy above are honored in every region; add maxConcurrentWorkflowInvocations and maxConcurrentActivityInvocations only for a project in a private region, where they are not dropped.
Workflow access policy
A workflow access policy is an allow-list that controls which caller apps can schedule workflows and activities on the apps in scopes. Without a policy, all cross-app workflow calls are allowed; once a policy covers an app, every cross-app call must match a rule or it is denied. Self-calls are always allowed. For the conceptual story and CLI walkthrough see Secure workflows with access policies.
Spec
spec:
rules:
- callers:
- appID: <caller-app-id>
workflows:
- name: <workflow-name or glob>
operations: [schedule]
activities:
- name: <activity-name or glob>
Field reference
| Field | Type | Required | Description |
|---|---|---|---|
rules | list | no | Allow-list of rules; a call is permitted if any rule matches. An empty or omitted list denies every cross-app workflow call to the scoped apps. |
rules[].callers | list | yes | Caller apps this rule grants access to. At least one entry. |
rules[].callers[].appID | string | yes | The ID of the calling application. |
rules[].workflows | list | no* | Workflow rules granted to the matched callers. |
rules[].workflows[].name | string | yes | Exact workflow name or glob pattern (*, ?, [abc]). |
rules[].workflows[].operations | string[] | yes | Set to [schedule]. The API also accepts terminate, raise, pause, resume, purge, get, and rerun for forward compatibility, but these operations don't route cross-app yet, so policies have no effect on them. |
rules[].activities | list | no* | Activity rules granted to the matched callers. Activities only support scheduling, so there is no operations field. |
rules[].activities[].name | string | yes | Exact activity name or glob pattern. |
* At least one of workflows or activities must be present in each rule.
Worked example
apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
name: order-app-policy
scopes:
- order-app
spec:
rules:
- callers:
- appID: orchestrator-app
workflows:
- name: order-processing
operations: [schedule]
- name: "report-*"
operations: [schedule]
activities:
- name: charge-payment
Applying and inspecting policies
# Apply or update
diagrid resiliency create -f resiliency.yaml --project <project>
diagrid resiliency update -f resiliency.yaml --project <project>
diagrid configuration create -f configuration.yaml --project <project>
diagrid configuration update <name> -f configuration.yaml --project <project>
diagrid apply -f configuration.yaml --project <project>
diagrid workflow access-policy create -f workflow-access-policy.yaml --project <project>
diagrid workflow access-policy update -f workflow-access-policy.yaml --project <project>
# Inspect
diagrid resiliency list --project <project>
diagrid resiliency get <name> --project <project>
diagrid configuration list --project <project>
diagrid configuration get <name> --project <project>
diagrid workflow access-policy list --project <project>
diagrid workflow access-policy get <name> --project <project>
# Remove
diagrid resiliency delete <name> --project <project>
diagrid configuration delete <name> --project <project>
diagrid workflow access-policy delete <name> --project <project>
A configuration can also be edited without a manifest. diagrid configuration create and diagrid configuration update take the same set of inline flags — --default-action, --policy, the --tracing-* flags, the --workflow-* and --activity-concurrency-limit flags, and the pipeline-handler flags — as an alternative to --file. On update, those flags merge section by section: a section is touched only when at least one of its flags is set, and fields you leave out keep their current value. To replace a configuration wholesale instead, pass --file or use diagrid apply -f.
See the diagrid configuration CLI reference for its full flag set, and run diagrid resiliency --help for the resiliency flags.
See also
- Policies: conceptual overview.
- Resiliency: retries, timeouts, and circuit breakers in depth.
- Apply resiliency policies: defining and binding resiliency policies end to end.
- Secure workflows with access policies: workflow access policies end to end.
- Secure service invocation with access control: service-invocation ACLs end to end.
- IDs: how policies are bound and managed day-2.