Skip to content

Policy: what a pipeline may be made of

Building one?

Step 3 of the backend track puts this page to work in a running backend: a policy per caller, both endpoints narrowed by it, and what the editor then shows the author.

The registry says what the process can execute. That is a different question from what a given pipeline may use. A platform that runs pipelines on behalf of several customers hands each of them a subset — these stages, these node types — and that subset is the boundary the author of the graph cannot argue with, because they only write the graph.

from stageflow import Context, Pipeline, Policy, Session

basic = Policy(
    stages={"LoadTicket", "ClassifyByRules", "Template"},
    node_types={"entry", "stage", "condition", "switch", "terminal"},
)

session = Session(id="run-1", pipeline=pipeline, context=Context(vars=vars),
                  policy=basic)

A policy is given to the session by the host, next to the debugger. It is not read from the pipeline JSON and nothing in the JSON can widen it — a graph that could raise its own allowance would not be an allowance.

None is not the empty set

The two mean opposite things, and the difference is the point:

Written Means
Policy() no opinion — everything the process has registered
Policy(stages=set()) nothing at all
Policy(stages={"A"}) stage A, and no other

A policy that forgot to list its stages must not silently grant every stage the process happens to have imported, so the default of each field is None and an empty set is taken literally.

Refused twice: at validation and while running

pipeline.validate(basic)
# PipelineValidationError: Pipeline validation failed:
#   work: stage 'LlmReply' is not allowed by the policy
#   loop: node type 'map' is not allowed by the policy

Validation collects everything the policy refuses rather than stopping at the first, because somebody saving a graph wants the list of what to change. Session validates on construction, so a pipeline with a forbidden stage never begins — the stage is not reached rather than stopped halfway.

The session also checks while running, on every node and before every stage. That second line matters for a graph built in Python and never validated, and it costs a set lookup that is skipped entirely when the field is None.

A refusal at run time is a PolicyViolationError, which is a PermissionError as well as a StageFlowError.

A subpipeline is not a way out

The child graph of a subpipeline node is checked twice, for different reasons. Validation walks the declared subpipelines in the JSON, nested ones included, and says where the trouble is:

[inner] w: stage 'LlmReply' is not allowed by the policy

Without that, "you may save this" and "you may not run it" would be hours apart. At run time the child graph becomes a Pipeline of its own and is validated again, with the parent's policy, which travels down exactly as the debugger does — so a nested graph is not a way out at either moment.

Telling a client what it may use

capabilities() takes a policy and narrows its answer to it:

capabilities(basic)
# {"stageflow": "0.13.0",
#  "node_types": ["condition", "entry", "stage", "switch", "terminal"],
#  "stages": 3}

That is what a backend should serve to an editor: the editor needs to know what this caller may draw, and whether a node type is missing because the core is older or because the allowance is narrower is not a distinction it has to make.

What a policy does not do

It restricts what a pipeline is made of, not how much it consumes. A graph of allowed stages can still loop, fan out or run for a long time — retry, map over a large list, parallel branches.

That is the other half, and it lives in the same object: Policy(limits=...), described in Limits. A plan is one value — which blocks, and how much of anything.