Skip directly to article

Current guidance · concepts baseline 0.2.0-preview.1

Executable lumber quickstart

How do I run one protected action through the Actions facade?

PREVIEW ACCESS

Use the selected preview versions for this example. Find your packages, or try the package-free Lumber guided tour.

This is a complete minimal Actions-facade program for Node 22. It uses only package-root imports from the accepted tuple: Core 1.0.0-rc.1, Runtime 0.2.0-preview.5, and Actions 0.2.0-preview.8. The local Map is a synthetic host-owned authoritative system; the example needs no service, credential, network access, or package implementation source.

The host constructs trusted context. tenant.securityDomainRef is the grounding security boundary, while authorization.scopeRef is a separate business-policy scope. Validation and grounding run before authorization, the provider alone mutates, and the independent verifier alone establishes PRESENT. The renderer joins the returned result to the trusted original request, saved claim plan, exact operation/effect/execution binding, authoritative truth, returned PERMITTED decision, matching safe-claim type and scope, with the subject bound in the saved request and any conflicting returned subject rejected before using the vetted sentence. A classification alone is never wording permission.

This synthetic program keeps its host record in memory and checks wrong-subject, wrong-proposition and wrong-execution cases. A real host must durably admit the request and persist its identity/claim-plan binding before provider dispatch; this quickstart does not prove restart safety. The returned initial safeClaims projection carries a claim type, not a separate proposition reference, and claims.subjectRef may be null, so the host compares the registered proposition with its trusted saved plan rather than inventing a returned proposition field. See Safe claims for the scope of this scenario-specific pattern.

Bind every claim to the requested effect

Put every claim-relevant dimension of the protected effect in requestedEffect and construct claim attributes from that exact effectFields set. Missing, extra, or changed fields fail closed as a blocked claim. The opt-in InitialExecutionClaimPlan is registration-owned, data-only, and bounded. It supplies three mutually exclusive alternatives—COMMITTED, DISPROVED, and UNKNOWN—rather than three simultaneous permissions. After one dispatch and authoritative verification, Runtime mechanically selects and evaluates only the branch matching effect truth; unused branches never become authority.

In this action, requestedEffect is exactly { product: 'cedar-8ft-gradeA', quantity: 20 }. Runtime derives protected claim fields from that retained state. A provider acknowledgment or successful-looking response grants no general claim permission, an authoritative DISPROVED does not authorize arbitrary negative wording, and an initial UNKNOWN does not create an ordinary safe claim. If Core permits the UNKNOWN alternative, it appears only in observationClaims with scope RELATED_INITIAL_VERIFICATION_OBSERVATION_ONLY: historical authority that the exact initial observation remained unresolved, not current-state, retry, redispatch, compensation, reconciliation, recovery, or future-state authority.

Without an initial plan, the default remains success-oriented and proposition-specific. Legacy claims.build remains a one-proposition compatibility path: Actions captures the callback at registration, and Runtime snapshots, validates, and freezes its returned proposition before provider dispatch. Later mutation of the caller-owned definition, callback property, or returned proposition cannot change what Core evaluates.

Install, compile, and run

Package access is not public. In an authorized artifact directory containing the three accepted tarballs, save the code below as lumber-quickstart.mts. Add this minimal package.json; the compiler and Node declarations are explicit consumer-only development dependencies, not AnswerUp runtime dependencies:

{
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc -p tsconfig.json"
  }
}

Add this tsconfig.json. Its include also compiles lumber-recovery.mts and lumber-grounding-diagnostics.mts when those companion examples are present:

{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "lib": ["ES2022"],
    "types": ["node"],
    "skipLibCheck": false,
    "outDir": "dist"
  },
  "include": ["*.mts"]
}

Then run exactly:

npm install --save-dev --save-exact --cache ./.npm-cache --ignore-scripts --no-audit --no-fund typescript@5.8.3 @types/node@22.15.30
npm install --save-exact --cache ./.npm-cache --ignore-scripts --no-audit --no-fund ./answerup-core-1.0.0-rc.1.tgz ./answerup-runtime-0.2.0-preview.5.tgz ./answerup-actions-0.2.0-preview.8.tgz
npm run build
node dist/lumber-quickstart.mjs

npm run build resolves tsc from this consumer's node_modules/.bin; no global compiler, ambient PATH, portal dependency, sibling repository, or undeclared npx download is used. The relative .npm-cache starts empty in a fresh consumer and is owned by that consumer.

Expected bounded output:

{"kind":"COMMITTED","effect":"PRESENT","dispatches":1,"message":"20 cedar boards were reserved."}

Complete program

import {
  createProtectedActionRuntime,
  defineProtectedAction,
  type ActorRef,
  type DeliveryIdentity,
  type EffectIdentity,
  type ExecutionIdentity,
  type HostInvocationIdentity,
  type OperationIdentity,
  type ProtectedActionResult,
  type SecurityDomainRef,
  type TenantRef,
  type TrustedContext,
} from '@answerup/actions';

type ReserveInput = { product: 'cedar-8ft-gradeA'; quantity: number };
type GroundedLumber = { subject: string; quantity: number };
type ReserveEffectFields = { product: ReserveInput['product']; quantity: number };

// Synthetic authoritative state owned by the host, not by AnswerUp.
const inventory = new Map<string, number>([['cedar-8ft-gradeA', 100]]);
let dispatches = 0;

// The host constructs trusted identity, tenant, policy, and provider context.
const context: TrustedContext = {
  tenant: {
    tenantRef: 'tenant:lumber-demo' as TenantRef,
    securityDomainRef: 'security-domain:lumber-demo' as SecurityDomainRef,
  },
  actor: { actorRef: 'actor:buyer-42' as ActorRef, assurance: 'host-session' },
  // Policy scope is deliberately different from the tenant security domain.
  authorization: { scopeRef: 'policy-scope:inventory-reservation', policyRevision: 'policy:7' },
  provider: { accountRef: 'inventory:local-fixture' },
  delivery: { deliveryId: 'delivery:quickstart-1' as DeliveryIdentity, source: 'application' },
  hostInvocation: { invocationId: 'invocation:quickstart-1' as HostInvocationIdentity },
};

// The host saves its original request and approved wording plan before dispatch.
const originalRequest = Object.freeze({
  tenantRef: context.tenant.tenantRef,
  actionName: 'reserve_lumber', actionVersion: '1',
  product: 'cedar-8ft-gradeA' as const, quantity: 20,
  subjectRef: 'lumber:cedar-8ft-gradeA' as string,
  committedProposition: 'reservation:committed',
});
const claimPlan = {
  propositions: {
    COMMITTED: { propositionRef: 'reservation:committed' },
    DISPROVED: { propositionRef: 'reservation:did-not-occur' },
    UNKNOWN: { propositionRef: 'reservation:initial-observation-unresolved' },
  },
};
type Binding = { operationIdentity: OperationIdentity; effectIdentity: EffectIdentity; executionIdentity: ExecutionIdentity };
const hostRecord: { request: typeof originalRequest; claimPlan: typeof claimPlan; binding: Binding | null } = {
  request: originalRequest, claimPlan, binding: null,
};

const reserveLumber = defineProtectedAction<ReserveInput, GroundedLumber, ReserveEffectFields>({
  name: 'reserve_lumber',
  version: '1',
  validateProposal(value) {
    if (!value || typeof value !== 'object') return { valid: false, kind: 'INVALID_PROPOSAL', code: 'INPUT' };
    const input = value as Record<string, unknown>;
    if (input.product !== 'cedar-8ft-gradeA' || !Number.isInteger(input.quantity) || Number(input.quantity) < 1) {
      return { valid: false, kind: 'INVALID_PROPOSAL', code: 'PRODUCT_OR_QUANTITY' };
    }
    return { valid: true, input: { product: input.product, quantity: Number(input.quantity) } };
  },
  ground(input, trusted) {
    const subject = `lumber:${input.product}`;
    // This exact set is the authority for every claim-relevant effect dimension.
    const effectFields: ReserveEffectFields = { product: input.product, quantity: input.quantity };
    const candidate = {
      referent: subject,
      source: 'authoritative' as const,
      // Each resolving candidate owns a non-empty resource discriminator.
      attributes: { resource: subject, product: input.product },
      evidenceRefs: ['catalog:snapshot:12'],
      observationRef: 'catalog:observation:12',
    };
    return {
      kind: 'GROUNDED',
      grounded: { subject, quantity: input.quantity },
      scope: trusted.tenant.securityDomainRef,
      subject,
      requestedEffect: effectFields,
      candidates: [candidate],
      observationRef: 'catalog:observation:12',
    };
  },
  authorize({ context: trusted, grounded }) {
    return trusted.authorization.scopeRef === 'policy-scope:inventory-reservation'
      && grounded.scope === trusted.tenant.securityDomainRef ? 'PERMIT' : 'DENY';
  },
  provider: {
    adapterIdentity: 'local-inventory-provider',
    capability: { capabilityId: 'lumber-reservation', majorVersion: 1 },
    async dispatch({ input, context: trusted, operationIdentity, effectIdentity, executionIdentity }) {
      // In a real host, persist this binding and admission before the provider call.
      if (hostRecord.binding || trusted.tenant.tenantRef !== hostRecord.request.tenantRef ||
          input.product !== hostRecord.request.product || input.quantity !== hostRecord.request.quantity) {
        throw new Error('trusted request or admission mismatch');
      }
      hostRecord.binding = { operationIdentity, effectIdentity, executionIdentity };
      dispatches += 1;
      inventory.set(input.product, (inventory.get(input.product) ?? 0) - input.quantity);
      return { kind: 'ACKNOWLEDGED' };
    },
  },
  // This is an independent read of host-owned authoritative state.
  verifier: {
    verifierIdentity: 'local-inventory-verifier',
    async verify({ input }) {
      return inventory.get(input.product) === 100 - input.quantity
        ? { kind: 'PRESENT', observationRef: 'inventory:read:13' }
        : { kind: 'INDETERMINATE', observationRef: 'inventory:read:13' };
    },
  },
  // Three mutually exclusive alternatives; only the authoritative branch is evaluated.
  initialExecutionClaimPlan: claimPlan,
});

const runtime = createProtectedActionRuntime();
runtime.register(reserveLumber);
const answeringRequest = Object.freeze({
  tenantRef: context.tenant.tenantRef, product: 'cedar-8ft-gradeA' as const, quantity: 20,
});
const result = await runtime.run<GroundedLumber>({
  action: 'reserve_lumber',
  input: { product: answeringRequest.product, quantity: answeringRequest.quantity },
  context,
});

if (result.kind !== 'COMMITTED' || result.effectTruth.status !== 'PRESENT' || dispatches !== 1 || result.trace.records.length !== 0) {
  throw new Error('quickstart invariant failed');
}

// This renderer is specific to this saved reservation and its vetted sentence.
const withheld = 'No outcome is available.';
function reservationMessage(
  outcome: ProtectedActionResult<GroundedLumber>,
  saved: typeof hostRecord,
  request: typeof answeringRequest,
): string {
  const binding = saved.binding;
  if (!binding || saved.request.tenantRef !== request.tenantRef ||
      saved.request.product !== request.product || saved.request.quantity !== request.quantity ||
      saved.request.actionName !== 'reserve_lumber' || saved.request.actionVersion !== '1' ||
      saved.request.subjectRef !== `lumber:${request.product}` ||
      saved.claimPlan.propositions.COMMITTED.propositionRef !== saved.request.committedProposition ||
      outcome.kind !== 'COMMITTED' || outcome.effectTruth.status !== 'PRESENT') return withheld;
  if (outcome.operation?.actionName !== saved.request.actionName ||
      outcome.operation.actionVersion !== saved.request.actionVersion ||
      outcome.operation.operationIdentity !== binding.operationIdentity ||
      outcome.core?.operationIdentity !== binding.operationIdentity ||
      outcome.core?.effectIdentity !== binding.effectIdentity ||
      outcome.core?.executionIdentity !== binding.executionIdentity ||
      outcome.effectTruth.effectIdentity !== binding.effectIdentity ||
      outcome.claims.effectIdentity !== binding.effectIdentity ||
      outcome.claims.executionIdentity !== binding.executionIdentity ||
      (outcome.claims.subjectRef !== null && outcome.claims.subjectRef !== saved.request.subjectRef) ||
      outcome.claims.scope !== 'historical_effect' ||
      outcome.claims.decision !== 'PERMITTED' ||
      outcome.claims.claimType !== 'EFFECT_COMMITTED' ||
      !outcome.claims.safeClaims.some(claim => claim.claimType === 'EFFECT_COMMITTED')) return withheld;
  return '20 cedar boards were reserved.';
}
const message = reservationMessage(result, hostRecord, answeringRequest);
if (message === withheld ||
    reservationMessage(result, { ...hostRecord, request: { ...originalRequest, subjectRef: 'wrong:subject' } }, answeringRequest) !== withheld ||
    reservationMessage(result, { ...hostRecord, claimPlan: { ...claimPlan, propositions: { ...claimPlan.propositions, COMMITTED: { propositionRef: 'wrong:proposition' } } } }, answeringRequest) !== withheld ||
    reservationMessage(result, { ...hostRecord, binding: { ...hostRecord.binding!, executionIdentity: 'wrong:execution' as ExecutionIdentity } }, answeringRequest) !== withheld) {
  throw new Error('claim join or negative-case invariant failed');
}

console.log(JSON.stringify({ kind: result.kind, effect: result.effectTruth.status, dispatches, message }));

UNKNOWN and process-loss continuation

For the two non-interchangeable reconciliation paths, immutable original UNKNOWN, source-digest/CAS authority, and a fresh-process verifier-only run, continue to the complete lumber recovery example. The grounding contract also runs the four preview.8 failure diagnostics with zero authorization and zero dispatch.