Terminal blocks end the flow. When the engine reaches one, it stops walking the block graph and returns a result. Nothing that comes after a terminal block in the same branch runs.
There are two terminal blocks: Allow and Deny. You'll also encounter issue_session and issue_tokens in the Session family — those are terminal too. The difference is that Allow and Deny are about the routing decision; Issue Session and Issue Tokens do the credential work.
Most flows don't end at Allow or Deny directly. The typical Login flow ends at Issue Session on the happy path and at Deny on a rejection. Allow is most useful as a sentinel in decision branches where one path accepts the request without issuing new credentials — for example, a step-up flow that confirms the user already meets the AAL requirement and can proceed.
Block id: allow · Available in: all flow types
"Terminal block that accepts the request. Used in pipelines that combine custom checks before falling through to the framework's default success path." Allow signals to the engine that this branch reached a successful end state. It does not issue a session JWT or OAuth2 tokens on its own — if you need those, place an Issue Session or Issue Tokens block before Allow, or replace Allow with Issue Session entirely.
Reads from state
- Nothing. Allow requires no inputs.
Writes to state
- Nothing. Allow produces no named outputs.
Use when — you're building a custom step-up flow and one branch has already verified the necessary factor, making further action unnecessary. Also useful in test flows where you want to confirm that a particular path reaches a successful terminal without issuing real credentials.
Block id: deny · Available in: all flow types
"Terminal block that rejects the request with a configurable reason code." Deny ends the flow with a rejection. You configure a machine-readable reason code (for example high_risk, brute_force_limit, or country_blocked) and an optional user_message. The reason code lands in the audit log so you can investigate rejections later. The user-facing response is always a generic error — the platform does not expose the reason code to the browser.
Reads from state
- Nothing. Deny requires no inputs; its behaviour is controlled by static config.
Writes to state
- Nothing. Deny produces no outputs because the flow ends immediately.
Use when — you want to reject a login from a Tor exit node, block a registration from a disposable email domain, enforce a country allowlist, or terminate any branch that should not produce a session. Pair Deny with a Decision block upstream: the Decision routes to Deny on the failing condition and to Issue Session (or the next stage) on the passing condition.
Cautions — The reason field is for your audit logs and your own dashboards; users never see it. If you want to show the user a specific message (for example, "Access from your country is not permitted"), set the user_message field. Keep user_message generic enough that it doesn't reveal internal policy details to a potential attacker.
The real-world terminal pattern
Section titled “The real-world terminal pattern”For most Cymmetri tenants, the end of a Login flow looks like this:
Stage: Post-Auth ├─ Block: Risk Evaluate ├─ Block: Decision (branch on severity) │ ├─ high → Deny (reason: "high_risk") │ ├─ medium → MFA → Issue Session │ └─ low → Issue Session └─ (end)Allow rarely appears here. It shows up when a flow checks whether the user already satisfies a condition and needs a clean "yes, proceed" terminal that doesn't re-issue credentials.