# Secure workflows with access policies

In a multi-application workflow, a parent workflow running on one app schedules child workflows and activities on another app. By default, any app in a Catalyst project can do this to any other app. Workflow access policies let you lock this down: you declare which caller apps are allowed to schedule which workflows and activities on a target app, and Catalyst denies everything else.

A `WorkflowAccessPolicy` is a project-level resource that you manage with the `diagrid workflow access-policy` commands or apply declaratively as a YAML manifest.

## How policies work

A policy has two parts:

- **Scopes** — the target apps the policy protects. Leave scopes empty to protect every app in the project.
- **Rules** — each rule grants a list of **callers** (apps) access to specific **workflows** and **activities** on the target apps. Names are matched exactly or as glob patterns: `*` matches any sequence of characters, `?` matches a single character, and `[abc]` matches a character set.

Policies are a pure allow-list. As soon as a policy covers an app, every cross-app workflow call to that app must match a rule — if no rule matches, Catalyst denies the call with a `PermissionDenied` error. Without any policy in place, all calls are allowed.

```mermaid
---
title: Workflow access policy evaluation
---
flowchart LR
  CALLER(Orchestrator App)
  subgraph Catalyst
    CID(orchestrator-app)
    POL{Access policy}:::decision
    OID(order-app)
  end
  TARGET(Order App)

  CALLER-->CID
  CID--"schedule order-workflow"-->POL
  POL--"allowed"-->OID
  OID-->TARGET

  classDef decision stroke:#ed8936
```

Three behaviors to keep in mind:

- **Self-calls are always allowed.** A policy only gates cross-app calls; an app can always schedule and manage its own workflows and activities, so it never needs to list itself as a caller.
- **Multiple policies combine.** When several policies cover the same app, a call is allowed if any rule in any of them matches.
- **Scheduling is what's enforced today.** Workflow rules require an `operations` field — set it 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. Activity rules have no `operations` field because activities only support scheduling.

## Create a policy

The fastest way to create a policy is with the `--scopes` and `--callers` flags:

```bash
diagrid workflow access-policy create order-app-policy \
  --project my-project \
  --scopes order-app \
  --callers orchestrator-app,reporting-app
```

This protects `order-app` and allows `orchestrator-app` and `reporting-app` to schedule every workflow and activity on it. Any other app in the project that tries is denied.

## Fine-grained rules with a YAML manifest

The `--callers` flag is all-or-nothing. To grant access to specific workflows and activities, define the policy as a YAML manifest and apply it:

```yaml
# order-app-policy.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
```

```bash
diagrid workflow access-policy create -f order-app-policy.yaml --project my-project
```

Here `orchestrator-app` can schedule the `order-processing` workflow, any workflow whose name starts with `report-`, and the `charge-payment` activity on `order-app` — and nothing else. Prefer exact names over glob patterns where you can; a broad pattern like `*` grants more than you might intend.

## Start from deny by default

For the strictest posture, apply a single policy with no scopes and no rules. It covers every app in the project, and because policies are a pure allow-list, it denies every cross-app workflow call:

```yaml
# default-deny.yaml
apiVersion: dapr.io/v1alpha1
kind: WorkflowAccessPolicy
metadata:
  name: default-deny
spec:
  rules: []
```

Each app can still run its own workflows and activities, since self-calls are always allowed. From this baseline, layer on narrowly scoped policies like the earlier examples to grant only the access each caller needs.

## Manage policies

List the policies in a project:

```bash
diagrid workflow access-policy list --project my-project
```

Inspect a policy, including its full rule set:

```bash
diagrid workflow access-policy get order-app-policy --project my-project -o yaml
```

Update a policy with flags. The flags replace the corresponding fields of the existing policy, so the example below replaces all existing rules with a single grant that allows `orchestrator-app` to schedule everything:

```bash
diagrid workflow access-policy update order-app-policy \
  --project my-project \
  --callers orchestrator-app
```

To change fine-grained rules, edit the YAML manifest and run `diagrid workflow access-policy update -f` — the manifest replaces the existing policy.

Delete a policy to lift its restrictions:

```bash
diagrid workflow access-policy delete order-app-policy --project my-project
```

## Verify a policy

To confirm a policy is enforced, schedule a workflow or activity from an app that no rule allows. The caller receives a `PermissionDenied` error:

```text
access denied by workflow access policy: app 'reporting-app' operation 'schedule' on workflow 'order-processing'
```

Calls that match a rule continue to succeed, and the target app never sees the denied requests.

## Next steps

- Explore [workflow patterns](https://docs.diagrid.io/develop/workflows/patterns) for chaining, fan-out/fan-in, and external events
- Learn how [Catalyst secures the platform end to end](https://docs.diagrid.io/concepts/security)
- See [policies](https://docs.diagrid.io/concepts/policies) for the other declarative policy types you can bind to apps
