Skip to content

Command pipeline

Every command passes through the same stages in the same order, whichever host delivered it. Knowing that order explains why a denied caller never sees rule messages and why /validate never touches your handler.

Failed, or no user for a protected operation

Not valid JSON

Not met

false

Denied

No

Blocking result

Results above the allowed severity

Yes

No

Request for an Arc route

Authentication handlers

401

Resolve tenant

Read and parse the body

400 malformedRequest

Declared authorization

403

Bind fields or schema

authorize callback if bound

403

Resolve command key and context values

Global authorization filters

403 without validation details

Input shape valid

400 malformedRequest

Global ordinary filters

400 or 500

Validators, then per-definition filters

400 with every result

Validation-only route

200, no execution

Begin scopes, provide, handle,

response values and operations, complete scopes

StepYou configure it withWhen it fails
1. Authenticationauthentication handlers, or a native principal401
2. Tenanttenancy.httpHeader, tenancy.sources, or tenancy.resolveDoes not reject by default; tenancy.required answers 400 and a membership check 403
3. Read the inputNothing400 malformedRequest
4. Declared authorization@roles, @authorize, @allowAnonymous, or authorization403
5. Bind the input@field declarations, or a Zod schemaA wrong shape is held until global authorization finishes, then 400 malformedRequest
6. Per-request authorizationauthorize(input, context) on a successfully bound low-level definition403
7. Resolve contextScoped context-value providers and key resolvers on valid inputA failure stops execution
8. Global authorization filtersScoped AuthorizationCommandFilter services403 for denial; a thrown filter fails closed
9. Global ordinary filtersScoped CommandPipelineFilter services400 for validation, 500 for exceptions
10. ValidationValidators, concept validators, then validate and per-definition filters400 with every result collected
11. ExecuteExecution scopes, provide(), handle(), response value handlers, operations400, 403, or 500 from the outcome
  • Global filters short-circuit at the first fragment that remains unsuccessful after severity filtering; existing per-definition callbacks still collect every result.
  • POST <command route>/validate stops after step 10. provide(), handle(), and execution scopes never run; execution runners wrap the validation stage.
  • Unparseable JSON is rejected in step 3. A parsed body with the wrong shape reaches global authorization before reporting 400. On binding failure context.command is raw input, and context value providers and key resolvers do not run. A denial never discloses validation results.
  • Step 1 answers 401 only when at least one authentication handler is configured. With none, nobody is authenticated and a protected operation answers 403 in step 4.
  • On valid input, context-value providers and key resolvers run before context-aware authorization filters so those filters can inspect the resolved key and values, as in .NET. These providers and resolvers are trusted preparation: they must not make protected business mutations. They are not command provide(), which runs only during execution. Do not assume that no application code runs before authorization; declared authorization and the bound authorize callback precede preparation, but global authorization filters follow it.
  • Filter services are resolved in the operation scope. Validator dependencies are constructed after global filters, including on /validate; handler dependencies are constructed only after validation succeeds. A missing dependency reports reason dependencyUnavailable.
  • A validator that throws produces 400 with reason validatorFailed and message Validation failed; the exception text is never sent, and the error goes to the configured logger.
  • Cancellation is an unconditional admission check before each stage and after each awaited resolution, even after an acknowledged commit: declared authorization and each named policy resolution/invocation, filters, validators, dependency resolution, provide(), handle(), each response-handler service resolution and invocation, and query perform() and render(). An in-flight callback is allowed to finish; no later stage starts. A completed command handler’s denied(...), rejected(...), or empty result is preserved without starting response handlers, even when unrelated handlers are registered. Ordinary returned values are classified against the actual registered handlers: handled values never become client responses, while a plain value that none handle can be the client response. Cancellation before a matching response handler starts produces a failure with no response, not a successful response containing its unhandled effect. If a response handler has acknowledged an irreversible effect, cancellation after it completes keeps success and the client response only when no later matching handler is waiting. If a later matching handler cannot start, the result has isSuccess: false, a cancellation exception, and no response; any already acknowledged effect remains committed, and backend recovery/operation-outcome diagnostics remain available. A completed execution runner likewise retains its actual result and diagnostics. Integrations must call acknowledgeCommandCommit(context) only after an authoritative acknowledgment (Chronicle does this after the append). Cancellation of the optional Chronicle projection-completion wait after an acknowledged append leaves the append successful with its response; the read model might not yet be updated. A genuine observer failure still fails the command.

The result envelope and status code follow the Arc HTTP contract. A result that fails at any step never carries a response value. Unless exposeExceptionDetails is enabled, exception messages and stack traces are replaced before serialization; the correlation ID stays.