# Policies

Reference for the declarative manifests that define Catalyst policies. For the conceptual story see [Policies](https://docs.diagrid.io/concepts/policies). For end-to-end usage see [Resiliency](https://docs.diagrid.io/concepts/policies/resiliency) and [IDs](https://docs.diagrid.io/operate/project-operations/ids).

Policies are applied with the `diagrid` CLI. The target project is provided by the `--project` flag, not by a field in the manifest:

```bash
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:

```yaml
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

```yaml
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

```yaml
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](https://docs.diagrid.io/operate/project-operations/policies/service-invocation).

### Spec

```yaml
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.maxConcurrentWorkflowInvocations`<br />`workflow.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.globalMaxConcurrentWorkflowInvocations`<br />`workflow.globalMaxConcurrentActivityInvocations` | The same ceilings across every replica, enforced by the scheduler rather than per sidecar. Omit for no limit. |
| `workflow.workflowConcurrencyLimits`<br />`workflow.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. |

:::warning
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

```yaml
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](https://docs.diagrid.io/operate/project-operations/policies/workflows).

### Spec

```yaml
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

```yaml
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

```bash
# 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`](./catalyst/cli-reference/configuration/configuration.md) CLI reference for its full flag set, and run `diagrid resiliency --help` for the resiliency flags.

## See also

- [Policies](https://docs.diagrid.io/concepts/policies): conceptual overview.
- [Resiliency](https://docs.diagrid.io/concepts/policies/resiliency): retries, timeouts, and circuit breakers in depth.
- [Apply resiliency policies](https://docs.diagrid.io/operate/project-operations/policies/resiliency): defining and binding resiliency policies end to end.
- [Secure workflows with access policies](https://docs.diagrid.io/operate/project-operations/policies/workflows): workflow access policies end to end.
- [Secure service invocation with access control](https://docs.diagrid.io/operate/project-operations/policies/service-invocation): service-invocation ACLs end to end.
- [IDs](https://docs.diagrid.io/operate/project-operations/ids): how policies are bound and managed day-2.
