effx
Authoring

Operations

Query, Command, Errors, Requirements, Rpc, Cli and Authorize. What each records, and which parts the compiler infers.

An operation is a Query or a Command plus optional annotations. Every other annotation attaches to an operation. Without @Query or @Command it has no target and the compiler reports EFFX1103.

Query and Command

Decorator and builder forms:

// decorator
@Query({ name: "User.Get", input: GetUserInput, success: User.Public })
static get(input: typeof GetUserInput.Type) { /* returns Effect<A, E, R> */ }

// builder
export const getUser = Operation.query({ name: "User.Get", input: GetUserInput, success: User.Public })
  .handler((input: typeof GetUserInput.Type) => /* Effect<A, E, R> */);

@Command(options) and Operation.command(options) take the same options.

OperationOptions

FieldTypeOptionalMeaning
namestringyesOperation name. Becomes the stable ID operation:<name>. Default: <Class>.<method> for a static method, the export name for a builder.
inputSchema.TopnoInput schema, recorded as a SchemaRef with an InputOf edge.
successSchema.TopnoSuccess schema, recorded as a SchemaRef with a SuccessOf edge.

Source: packages/runtime/src/Annotation.ts (OperationOptions); default name from packages/compiler/src/extensions/core.ts (operationName).

Query is a semantic claim

@Query means "the developer declares observational semantics". It is not a purity proof: two handlers can share A, E, R while one reads and one writes (ADR 0006, docs/decisions/0006-query-is-a-semantic-claim.md). The compiler checks only what it can know.

Query doing...ResultStatus
exposed over non-GET HTTPEFFX2401 errorImplemented (core.ts queryOverNonGet).
exposed over POST with payloadIsQuery: true and a payloadallowedImplemented; opt-in per ADR 0010. See HTTP contract.
calling a Command, a write-classified service, or a write leaseerror or warning (ADR 0006)Not implemented. core.ts has no such analysis. ADR 0006 marks it "(future)".
an arbitrary raw Effectunknown; no claimBy design.

Query and Command are the only operation kinds. There is no Mutation or Action (ADR 0006).

Errors and Requirements

E and R are inferred from the handler's Effect<A, E, R> return type. @Errors and @Requirements assert them (ADR 0005, docs/decisions/0005-e-and-r-are-inferred-declarations-assert.md).

SyntaxSignatureArguments
@Errors(...) / .errors(...)(...schemas: ReadonlyArray<Schema.Top>)Schema-addressable error classes.
@Requirements(...) / .requirements(...)(...services: ReadonlyArray<ServiceLike>)Context.Service classes.

ServiceLike:

FieldTypeOptionalMeaning
keystringnoThe static key carried by a Context.Service class.

Source: packages/runtime/src/Annotation.ts (ServiceLike), packages/runtime/src/decorators.ts, packages/runtime/src/builder.ts.

Inferred versus asserted

SourceIRCheck
handler return type onlyerrors / requirements with inferred: truean opaque constituent is a diagnostic asking for a mapping
@Errors(...) / @Requirements(...)inferred: false, the declared setexact set equality against the inferred set
CodeSeverityCondition
EFFX2201errorhandler can fail with an error that @Errors does not declare
EFFX2202error@Errors declares an error the handler cannot fail with
EFFX2203errorinferred error is not schema-addressable (for example a plain Error); map it with @Errors
EFFX2204warninginferred errors are empty; the boundary will expose none
EFFX2302errorhandler requires a service that @Requirements does not declare
EFFX2303error@Requirements declares a service the handler no longer requires
EFFX2304errorinferred requirement has no stable effx identity; declare or register the service

Source: packages/compiler/src/extensions/core.ts; ADR 0005; docs/specs/0002-source-frontend.md §"E/R inference".

Dropping an assertion never silently changes a contract: the inferred set takes over, and an opaque constituent still diagnoses.

Effect.gen bodies that fail with both a tagged error and a plain Error infer E = Error by common-supertype inference. effx reports that as EFFX2203. This is documented in spec 0002 and not solved by the compiler.

External operations

On a .declare() operation (see HTTP) there is no handler to infer from. @Errors and @Requirements are then declarations, not assertions: they are recorded with inferred: false, including the Requires edge, and the E/R equality checks do not run. Without them the sets are empty and inferred: false. Source: core.ts (operationInterpreter, errorsAssertion, requirementsAssertion), docs/specs/0010-target-profiles-and-emit-modes.md §"Declaration-only operations".

Rpc and Cli

AnnotationSignatureRecords
@Rpc(name) / .rpc(name)(name: string)an Exposure with transport rpc and that name
@Cli(words) / .cli(words)(words: string)an Exposure with transport cli; words is split on whitespace into the command words
@Rpc("User.Get")
@Cli("users get")

Source: packages/runtime/src/decorators.ts, packages/runtime/src/builder.ts, packages/compiler/src/extensions/transports.ts.

A .declare() operation cannot have RPC or CLI exposure: a raw HTTP binding cannot implement them (EFFX1107).

Authorize

SyntaxSignatureRecords
@Authorize(capability) / .authorize(capability)(capability: Capability)a Capability node, an AuthorizedBy edge from the operation, and a Focus node plus Focuses edge when the capability has a focus

Capability values come from Capability.make. See Persistence and authority for the field table.

@Authorize records authority facts only. No authorization runtime exists: Cedar and request-scoped leases are deferred past the MVP kernel (ADR 0007, docs/decisions/0007-cedar-authorizes-effx-issues-leases.md). HTTP access declarations are a separate, app-enforced contract; see Problems and access.

Example

examples/users/src/operations.ts
import { Authorize, Cli, Command, Errors, Http, Query, Requirements, Rpc } from "@effx/runtime";
import { Effect } from "effect";
import { ChangeEmail, Read } from "./capabilities.ts";
import { ChangeEmailInput, EmailTaken, GetUserInput, UserNotFound } from "./schemas.ts";
import { Users } from "./services.ts";
import { User } from "./user.ts";

export class UserOperations {
  @Query({ name: "User.Get", input: GetUserInput, success: User.Public })
  @Http.Get("/users/:id")
  @Rpc("User.Get")
  @Cli("users get")
  @Authorize(Read)
  static get(input: typeof GetUserInput.Type) {
    return Effect.gen(function* () {
      const users = yield* Users;
      const user = yield* users.find(input.id);

      return { id: user.id, displayName: user.displayName };
    });
  }

  @Command({ name: "User.ChangeEmail", input: ChangeEmailInput, success: User.Self })
  @Http.Patch("/users/:id/email")
  @Rpc("User.ChangeEmail")
  @Cli("users change-email")
  @Authorize(ChangeEmail)
  @Errors(UserNotFound, EmailTaken)
  @Requirements(Users)
  static changeEmail(input: typeof ChangeEmailInput.Type) {
    return Effect.gen(function* () {
      const users = yield* Users;

      return yield* users.setEmail(input.id, input.email);
    });
  }
}

User.Get declares only the Query, transports and capability: E and R are inferred. User.ChangeEmail also asserts @Errors(UserNotFound, EmailTaken) and @Requirements(Users).

On this page