cratis-arc-command · git:20261012.81f7c14 · 2026-10-12 · sha256 31d981999b513561

cratis-arc-command git:20261012.81f7c14A

Immutable. This exact content never changes and is served at /api/v1/blob/31d981999b513561.

---
name: cratis-arc-command
description: Define a Cratis Arc command — the [Command] record, its Handle() method and return shape, the optional Provide() step, and the generated TypeScript proxy. Use when adding a command, choosing what Handle() should return, deciding which values may reach the causation chain (including API keys and other credentials passed to a command, which need [NotAudited] and, when retained, [Encrypted]), or wiring a form or button to an Arc backend. Do not use for a validation-only change or merely to execute an existing command.
license: MIT
---

# Define an Arc command

A command is a record that carries the user's intent and owns its own handler.
Arc discovers it, runs authorization and validation, calls `Handle()`, and turns
whatever `Handle()` returns into events, server-executed operations, a response,
or a combination.

- An accepted `.play` model under the model root covers the behavior, or the
  repository is opted in (the root holds a committed `.play` file (`git ls-tree -r --name-only HEAD` lists a `.play` file there, narrowed to `-- <root>` when a root is configured), or the project set
  `mcpServers.screenplay.root` in `.cratis/ai.json`; an empty directory, install
  output, an uncommitted `.play` draft or a `.play` file outside the root does not count; master definition:
  `cratis-screenplay-modeling-lifecycle`): change the model first with
  `cratis-screenplay-event-modeling`. If the Screenplay skills are not installed,
  say so and do not author `.play` from memory.
  Edit code here only for infrastructure, clients, adapters, Screenplay code
  attachments, or gap-fill scope (`cratis-screenplay-render-and-gap-fill`);
  never edit Stage-managed output.

## Verified product sources

| Package | Version | Purpose |
| --- | --- | --- |
| `Cratis.Arc.Core` | `22.75.0` | `Cratis.Arc.Commands.ModelBound.CommandAttribute`, `CommandResult`, `CommandValidator<T>` |
| `Cratis.Arc.Chronicle` | `22.75.0` | event append handling, `Cratis.Arc.Chronicle.Commands.NotAuditedAttribute` |
| `Cratis.Arc.ProxyGenerator.Build` | `22.75.0` | `CratisProxiesOutputPath` MSBuild integration |
| `Cratis.Fundamentals` | `7.23.0` | `Cratis.Monads.Result<TResult, TError>` |
| `Cratis.Chronicle` | `18.3.0` | `EventTypeAttribute`, `EventForEventSourceId`, `ICanProvideEventSourceId` |
| `@cratis/arc` | `22.75.0` | `ICommand`, `CommandResult`, `ValidationResult` |

> Arc surface rechecked at v22.75.0, including the source-pinned references below.
> Arc's package dependency is Chronicle 19.39.1 / Fundamentals 7.23.0; separately
> version-qualified Chronicle/Fundamentals claims retain their own verification.

Fundamentals rechecked at v7.23.0 in
[`Result<TResult, TError>`](https://github.com/Cratis/Fundamentals/blob/v7.23.0/Source/DotNET/Fundamentals/Monads/Result%7BTResult,TError%7D.cs):
success is first, error second, with implicit conversions from both types.

Reverify before claiming support for another version. Arc without Chronicle is a
supported setup; everything on this page that appends events needs Chronicle.
Operation guidance below is rechecked at **Arc v22.75.0**; see
`cratis-arc-command-operation` for its source and recovery contract.

## Declare the command

```csharp
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Events;

namespace <NamespaceRoot>.<Feature>;

[Command]
public record <ImperativeName>(<ConceptType> <Name>, <ConceptType> <Name>)
{
    public <EventType> Handle() => new(<Name>);
}

[EventType]
public record <EventType>(<ConceptType> <Name>);
```

Rules the framework actually enforces:

- `[Command]` is `Cratis.Arc.Commands.ModelBound.CommandAttribute` and applies to
  a class declaration only. A type counts as a command when it carries
  `[Command]` **and** declares a `Handle` method; either one alone is not enough.
- `Handle` is found by name on the command type itself. `ARC0003` is an error
  when another class defines `Handle` for a command, and `ARC0004` is an error
  when a `[Command]` type has no public instance `Handle`.
- `ARC0002` warns when a type has `Handle` but no `[Command]`; `ARC0007` warns
  when a command is a `class` rather than a `record`.
- For returned operations, `ARC0016` checks method shapes, `ARC0017` rejects bare
  operation collections, and `ARC0018` checks generated-invoker accessibility.
- Name the command as the action — `OpenDebitAccount`, not
  `OpenDebitAccountCommand`.
- Unmarked `Handle` parameters resolve from `Provide()` or DI; ask for services
  there rather than through a constructor. Server-owned new values use
  `[GeneratedValue]` instead (below).
- Never inject `IEventLog` into `Handle` to append the command's own events —
  `ARCCHR0007` warns, and it bypasses Arc's append pipeline. Express the append
  through the return type.

Use concept types rather than raw primitives for the command's values, and give
Chronicle-backed identities the `EventSourceId<T>` base — see the
`cratis-fundamentals-concept` skill for the exact shapes.

## Choose the return shape

`Handle()`'s return value is dispatched by its runtime type. The shape decides
what is appended and what the caller gets back. Operations are the exception:
the *declared* return type must name `ICommandOperation`/`CommandOperations`
(directly or in a tuple, `Task`, `Result`/`OneOf`) for Arc to execute them.

| Return | What Arc does |
| --- | --- |
| A registered event record | Appends it to the command's event source id |
| `IEnumerable<object>` of registered events | Appends each to the command's event source id |
| `EventForEventSourceId(id, @event)` | Appends that one event to `id` |
| A collection mixing events and `EventForEventSourceId` | Appends each to its own target, wrappers to their id and plain events to the command's |
| `ICommandOperation` (or a concrete operation) | Executes server-side after returned events are enrolled; never the response |
| `CommandOperations` | Executes an explicit batch sequentially after event enrollment; never the response |
| A tuple | Each element is dispatched on its own; the one element nothing can handle becomes the response |
| Anything else | Becomes the response payload |
| `Result<TEvent, ValidationResult>` | Success appends the event; failure becomes a validation failure |
| `Task<T>` / `ValueTask<T>` of any of the above | Awaited first, then dispatched |

When choosing a return shape or diagnosing dispatch failures, read [handler shapes](references/handler-shapes.md) for the dispatch order, the
tuple rule, and the exact failure modes.

⚠️ **An unregistered event type is not an error.** No handler claims it, so it
silently becomes the HTTP response body instead of being appended. A command
that "runs fine" but appends nothing is almost always a missing `[EventType]`.

For immediate inline side effects chosen by the command, prefer returning an
operation rather than calling the service inside `Handle()`. Arc executes the
work and selects optional compensation from the commit facts. Direct service
calls remain supported when their result is needed for the decision; durable
after-commit work belongs in reactors/outboxes/workflows. Use
`cratis-arc-command-operation` rather than a custom response handler or rollback
stack for this application work.

**Never return a nullable event or `null` to mean "nothing to do".** Arc only
processes a non-null result, so a `null` is success with nothing appended and the
caller is told a command succeeded that never happened. Missing state or a failed
rule is a rejection: return `Result<TEvent, ValidationResult>` with
`ValidationResult.Error(...)`, or reject in a validator or `Provide()`. Optional
`ICommandOperation` returns are a separate contract and unaffected. `ARCCHR0015` warns on nullable event returns. A nullable
read-model parameter (`ARC0006`) is handled by rejecting, never by returning a
nullable event.

Two consequences worth knowing before writing the first command:

- **The tuple is how a create command returns its new id.** An
  `EventSourceId`-derived value in the tuple is not appendable, so it becomes the
  response — and the response, when it is an event-source id value, is also what
  the events in that same tuple are appended to. Returning more than one
  unhandleable element throws `MultipleUnhandledTupleValues`. When the command
  also returns an operation keyed by that id, have the caller supply the id
  instead of generating a new value, so a retried request reuses the same
  ownership key. A generated value alone does not provide idempotency.
- **Events never carry their own event source.** Cross-stream writes use
  `EventForEventSourceId`; they are not expressed by a property on the event.

```csharp
[Command]
public record Register<Thing>(<ConceptType> <Name>)
{
    public (<ThingId>, <ThingRegistered>) Handle([GeneratedValue("thing")] <ThingId> id) =>
        (id, new(<Name>));
}
```

```csharp
[Command]
public record Transfer(<AccountId> From, <AccountId> To, <Money> Amount)
{
    public IEnumerable<object> Handle() =>
    [
        new EventForEventSourceId(From, new <Withdrawn>(Amount)),
        new EventForEventSourceId(To, new <Deposited>(Amount)),
    ];
}
```

## Generate a server-owned new identifier

**Arc 22.66.0+**, verified at **v22.75.0**: the create-command example above
requires `<ThingId>` to implement `IGeneratable<ThingId>` with a static `New()`
strategy (Fundamentals 7.23.0+; see `cratis-fundamentals-concept`). Replace
`Guid.NewGuid()` or `<ThingId>.New()` inside `Handle()` with a generated parameter,
not a new request property. Return the id as the ordinary response or in the
id/event tuple so the caller receives it.

`[GeneratedValue]` is in `Cratis.Arc.Commands.ModelBound` and applies only to
parameters of a non-static command `Handle()`. Arc generates after `Provide()`
control results permit execution, validates the concept, then invokes `Handle`.
`Provide()` and DI cannot override it. Use a non-nullable self-generatable type,
not raw `Guid`; names default to the parameter name or an explicit attribute name
and are case-sensitive, non-empty and unique within the command.

Keep client-chosen ids/references as inputs and services/read models as ordinary
dependencies. A UUID is not a secret, token, authorization proof, idempotency key,
or a gapless/sequential invoice number. For validation failures, captures,
ARC0022–ARC0025, migration and the pinned-spec workflow, read
[generated values](references/generated-values.md).

## Where the event source id comes from

Chronicle resolves the command's event source id in this order:

1. `commandContext.Response`, when the response is an event-source-id value —
   the tuple case above;
2. `ICanProvideEventSourceId.GetEventSourceId()` on the command;
3. a single `EventSourceId`/`EventSourceId<T>`-derived or `[Key]`-marked
   property on the command;
4. otherwise a freshly generated id.

A command with more than one candidate property is ambiguous and `ARCCHR0002`
warns; implement `ICanProvideEventSourceId` to say which value wins. Do not rely
on property order.

`[Key]` on a positional record's primary-constructor parameter is recognized
and counted once. Child-only/nested read-model types are not standalone
command-scoped models. `[EventSource<TSource>(stream)]` supplies source type and
stream metadata on commands and aggregate roots.

Read [decisions and routing](references/decisions-and-routing.md) when a command
needs guarded state, stream-id templates, definition-backed routes, event tags or
post-commit `CompleteStream`. Plain read-model injection remains advisory;
`Result<,>` alone is not concurrency protection. Explicit routing on a returned
`EventForEventSourceId` wins over command context.

Two attributes are spelled `[Key]`. Chronicle reads `Cratis.Chronicle.Keys.KeyAttribute`.
Marking `System.ComponentModel.DataAnnotations.KeyAttribute` in an application
that uses Chronicle compiles, resolves nothing, and silently invents a new event
source id for every command — `ARCCHR0008` reports it.

## Fetch what the handler needs with `Provide()`

`Provide` is an optional instance method that runs before `Handle`. Its purpose
is to keep IO out of `Handle`: it fetches or computes values, and Arc binds them
to `Handle`'s parameters by type.

```csharp
[Command]
public record Open<Thing>(<ThingId> Id, <OwnerId> OwnerId)
{
    public async Task<Result<<Owner>, ValidationResult>> Provide(IReadModels readModels)
    {
        var owner = await readModels.GetInstanceById<<Owner>>((EventSourceId)OwnerId);
        return owner is null
            ? ValidationResult.Error("Owner must exist.")
            : owner;
    }

    public <ThingOpened> Handle(<Owner> owner) => new(owner.Id);
}
```

- Short-circuit with `ValidationResult.Error(...)`; do not throw for a rejection
  the user can act on. A thrown exception surfaces as `HasExceptions` and HTTP
  500, not as a validation failure.
- Every value `Provide` returns must be consumed by a `Handle` parameter.
  `ARC0005` warns otherwise. `ValidationResult`, `AuthorizationResult` and
  `CommandResult` are exempt because they short-circuit rather than feed `Handle`.
- `Provide` runs after authorization and validation.

An Arc read model can also be injected directly into `Provide`, `Handle`, or a
`CommandValidator<T>` — but only by **the command's own resolved key**. Reading a
read model keyed by anything else needs an explicit by-id read, and an absent
instance does not always arrive as `null`. Read
[read-model injection](references/read-model-injection.md) before relying on a
directly injected read model.

## Decide what the causation chain may record

Every property value of the command is written to the causation of every event
the command appends, and the event log is immutable. Decide this when the
property is added.

| Marking | Use for | Effect |
| --- | --- | --- |
| `[PII]` (Chronicle) | personal data | encrypted in the event and enrolled in erasure; already withheld from causation |
| `[Encrypted]` (Chronicle, `Cratis.Chronicle.ProtectedValues`, 19.32.0+) | an operational secret the system retains — a third-party API key, a company's bank account number | encrypted at rest on the event, no erasure; does not withhold it from causation |
| `[NotAudited]` (Arc Chronicle) | the same operational secret, and any credential given as a command input | withheld from causation, nothing else |

A retained operational secret needs **both** `[Encrypted]` and `[NotAudited]`; either alone leaves a gap. A credential used for authentication (password, token) is marked `[NotAudited]` on the command and reaches an event only as a hash or a reference, never encrypted. Never combine `[PII]` with `[Encrypted]` (`CHR0053`).

```csharp
[Command]
public record Change<Secret>(
    <UserId> User,
    [property: NotAudited] string Old<Secret>,
    [property: NotAudited] string New<Secret>)
{
    public <SecretChanged> Handle(I<Hasher> hasher) => new(hasher.Hash(New<Secret>));
}
```

`[NotAudited]` applies to a class, struct, property, or parameter. On the type it
withholds every property at once. Marking the concept type instead makes it
travel to every command that takes that value. The command is still named on the
chain either way; only the values are withheld.

`ARCCHR0009` warns when a property's *name* reads like a secret and is unmarked.
It cannot see a secret whose name does not say so, so a clean build means
"nothing obvious was missed", not "no secrets are recorded". When the name only
reads like a secret and the value is safe to record, the framework's own guidance
is to mark it `[NotAudited]` anyway or rename the property — the value is written
either way, so the reading is all a reviewer has to go on.

## Generate the TypeScript proxy

```xml
<PackageReference Include="Cratis.Arc.ProxyGenerator.Build" Version="22.75.0" />

<PropertyGroup>
  <CratisProxiesOutputPath>$(MSBuildThisFileDirectory)../<Web>/src/api</CratisProxiesOutputPath>
</PropertyGroup>
```

`dotnet build` runs the generator after the build, and only when
`CratisProxiesOutputPath` is set. Output folders mirror the C# namespace, not the
file path. When configuring proxy generation or diagnosing missing or stale output, read [proxy generation](references/proxy-generation.md) for the full
set of MSBuild knobs and the common failures.

`[BlockOnValidationSeverity(ValidationResultSeverity.Warning)]` declares the
lowest blocking severity for validation, `Provide` and execution. Caller
`allowedSeverity`/`X-Allowed-Severity` can only make it stricter; `Unknown` blocks.
The proxy carries `blockOnValidationSeverity` and honors it client-side.

## The generated client contract

The generated proxy is a `Command` from `@cratis/arc`. Its members are the same
whatever renders it:

| Member | What it does |
| --- | --- |
| `route` | The generated route, including the configured API prefix |
| `roles` | The roles the command declares; empty when it declares none |
| `<property>` | Get or set one value; setting raises `propertyChanged` |
| `hasChanges` | True when any value differs from the initial values |
| `execute(allowedSeverity?, ignoreWarnings?)` | Sends the command; resolves to `CommandResult` |
| `validate()` | Runs authorization and validation on the server without the handler |
| `validateClientSide()` | Runs only the extracted rules locally; never touches the network |
| `setInitialValues(values)` | Sets the change-tracking baseline |
| `setInitialValuesFromCurrentValues()` | Rebaselines onto the current values |
| `revertChanges()` / `clear()` | Restore the baseline / reset everything |

Read `references/command-result.md` when branching on a command result or matching validation failures to fields. Branch on the specific flag, not only on `isSuccess`. The exact `CommandResult`
and `ValidationResult` shapes are in
[command result](references/command-result.md) — in particular, a validation
failure carries `members: string[]` (camelCased) and a numeric `severity`, not a
`propertyName` string.

`validateClientSide()` runs only the rules the generator could extract, so it can
pass where `execute()` still fails validation — read
[proxy generation](references/proxy-generation.md) when checking which validation rules are extractable.

Binding this proxy into a React component — the generated `use()` hook and the
Cratis Components command dialog and form fields — belongs to the Arc React and
Components guidance, not to this skill.

## Route near misses

- Adding or changing a rule on an existing command: the Arc command validation
  guidance.
- Executing an existing command from backend code: `cratis-arc-command-execution`.
- Declaring inline work, compensation, or operation specs:
  `cratis-arc-command-operation`.
- Append-time uniqueness or concurrency constraints: the Chronicle event
  constraints guidance.
- Choosing the concept or identity type for a value: `cratis-fundamentals-concept`.

## Verify

- The command is a `record`, carries `[Command]`, and declares a public instance
  `Handle`.
- The return shape matches what the command is supposed to do, and any
  cross-stream event is wrapped in `EventForEventSourceId`.
- At most one tuple element is unhandleable.
- Inline work follows `cratis-arc-command-operation`: explicit operation return
  shapes, no duplicate direct write, and tested execution/recovery boundaries.
- The event source id resolves from exactly one place; `ARCCHR0002` is silent.
- `[Key]`, where used, is `Cratis.Chronicle.Keys.KeyAttribute`.
- No `IEventLog` is injected into `Handle`.
- Every value `Provide` returns is consumed by an unmarked `Handle` parameter.
- Generated parameters follow [generated values](references/generated-values.md)
  and are pinned before scenario execution (`cratis-specifications-csharp`).
- User-correctable rejections are validation results, never thrown exceptions;
  `GeneratedValueRejected` instead signals a defective server generation strategy.
- Every secret or personal value is marked before it can reach the event log.
- `dotnet build` is clean in Debug and Release, with no suppressed `ARC*` or
  `ARCCHR*` diagnostic left unjustified.
- The generated proxy exists at the configured output path and the frontend
  compiles against it.