Effects and Sequencing#

An APL rule does something. That something is an effect. Effects are the building blocks of policy: a pre_invocation: block is an ordered list of them, and they run in sequence until one denies.

The effects#

EffectWhat it does
allowNo-op. Continue to the next effect.
deny / deny('reason') / deny('reason', 'code')Halt the phase and all later phases with a violation.
plugin(name) (alias run(name))Invoke a registered plugin (PII scan, audit log, custom check).
delegate(name, ...)Mint a downstream credential via a delegator plugin. See Delegation.
require_approval(name, ...) / confirm(...) / require_step_up(...) / require_attestation(...) / request_info(...) / require_review(...)Ask a human and suspend the operation until they respond. See Elicitation.
taint(label[, scope])Attach a label to the session or message. See Session Tainting.
restrict: { ... }Narrow the set of backends the router may select from. See Backend Restriction.
field pipelinesValidate or transform args/result fields. See APL.
PDP call (cedar:, cel:, opa(...))Delegate the decision to a policy engine. See PDP Integration.

Sequencing and halt-on-deny#

Effects in a pre_invocation: block run top to bottom. The first deny halts the phase and skips every later phase, so order is a tool: put cheap gates first and expensive effects last.

authorization:
  pre_invocation:
    - "require(role.hr)"                                  # cheap attribute gate
    - cedar:                                              # relationship decision
        action: 'Action::"read"'
        resource:
          type: Repo
          id: ${args.repo_name}
    - "delegate(github-oauth, target: github-api, permissions: [repo:read])"  # expensive, last

If require(role.hr) denies, the Cedar call and the token exchange never run. This is both faster and safer: you do not mint a credential for a caller you were going to reject.

Reactions: on_allow and on_deny#

A PDP call can carry reaction blocks that run depending on the decision:

authorization:
  pre_invocation:
    - cedar:
        action: 'Action::"read"'
        resource:
          type: Document
          id: ${args.doc_id}
      on_allow:
        - "taint(cedar_approved, session)"
      on_deny:
        - "deny('not permitted by Cedar policy', 'cedar_denied')"

on_allow runs its effects when the PDP permits; on_deny runs when it denies. Without an on_deny, a PDP denial halts the phase on its own.

Composition: sequential and parallel#

Effects can be grouped. sequential runs its members in order and halts on the first deny. parallel runs independent gates concurrently; any deny fails the group, and accumulating effects from the branches (taints, backend restrictions) all take hold.

authorization:
  pre_invocation:
    - parallel:
        - "require(perm.read_pii)"
        - cel: { expr: "subject.department == 'compliance'" }

parallel is for independent decisions only. It rejects field operations and delegation, because a discarded branch would silently lose those effects. Use sequential (the default for a pre_invocation: list) whenever one effect depends on another.

Phases recap#

Effects run within the four route phases: args, authorization.pre_invocation, result, authorization.post_invocation (see APL). delegate, elicitation verbs, and PDP calls belong in pre_invocation or post_invocation; field pipelines belong in args and result. A deny anywhere halts the rest. An elicitation verb can also suspend a phase — the operation neither allows nor denies, but pauses for a human and resumes on retry (see Elicitation).