Guide

Durable Publication with Resource.Output

Convert process-local Activity output material into a durable Resource key before publishing Done.

Some work does not merely compute a value—it prepares something outside the chart. Uploading an artifact, inserting a record, issuing a credential, or storing model output begins with process-local material and ends with a durable key that future work can name.

Committing the material would put the wrong value in durable history. Publishing a key before the external system has accepted it would publish a lie. Resource.Output makes that boundary explicit: the Activity returns material, the provider prepares it, and Motive does not publish the Activity's Done event until every prepared key has been durably settled as Publish.

Publication crosses the boundary in three steps

This is the reverse of Resource.Input. Input starts with a durable key and acquires a process-local value. Output starts with process-local material and prepares a durable key. Both directions remain owned by one Resource definition and one exact provider occurrence.

The example below issues token evidence. Watch the Activity produce raw material, the provider prepare evidence-1, and the chart remain unpublished until the provider confirms Publish.

Turn local material into durable identity

Success waits for durable publication.

The Activity produces process-local material. Its provider prepares a durable key and confirms publication before Motive releases the key-only success event.

1 · Declare material separately from the key

Resource.Output marks material that must be durably published.

TokenEvidence owns a durable key, process-local output material, and an acquired resource shape. IssueToken returns TokenEvidence.Output, so its handler produces raw material while its published Done contract carries only the prepared key.

import { Activity, Resource, State, Statechart, States } from "@motive/motive";
import { Schema } from "effect";

export const TokenEvidence = Resource.make("TokenEvidence", {
  key: Schema.String,
  material: Schema.Struct({ rawToken: Schema.String }),
  resource: Schema.Struct({ claims: Schema.Array(Schema.String) }),
});

export const IssueToken = Activity.make("IssueToken", {
  success: Schema.Struct({ evidence: TokenEvidence.Output }),
});

export class TokenIssue extends State.Compound<TokenIssue>()("TokenIssue") {
  static states = States.make(() => [Idle]);
}

class Idle extends State.Atomic<Idle>()("Idle") {}

export const TokenIssueChart = Statechart.make(TokenIssue);

2 · Mount the publishing provider

The coactive provider owns preparation and publication.

EvidenceStore dominates IssueToken while Issuing is active. The runner prepares the handler's Output material at that exact provider, commits a key-only manifest, and waits for publication before Done can move the chart to Complete.

import {
  Activity,
  Resource,
  State,
  Statechart,
  States,
  Transitions,
} from "@motive/motive";
import { Schema } from "effect";

export class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}

export const TokenEvidence = Resource.make("TokenEvidence", {
  key: Schema.String,
  material: Schema.Struct({ rawToken: Schema.String }),
  resource: Schema.Struct({ claims: Schema.Array(Schema.String) }),
});
const EvidenceStore = TokenEvidence.as("EvidenceStore");

export const IssueToken = Activity.make("IssueToken", {
  success: Schema.Struct({ evidence: TokenEvidence.Output }),
});

export class TokenIssue extends State.Compound<TokenIssue>()("TokenIssue") {
  static states = States.make(() => [Idle, Issuing, Complete]);
}

class Idle extends State.Atomic<Idle>()("Idle") {
  static transitions = Transitions.make(this, ({ on }) => [on(Begin, Issuing)]);
}

class Issuing extends State.Atomic<Issuing>()("Issuing") {
  static transitions = Transitions.make(this, ({ invoke, on, provide }) => [
    provide(EvidenceStore),
    invoke(IssueToken),
    on(IssueToken.Done, Complete, ({ event }) => event.value),
  ]);
}

class Complete extends State.Done<Complete>()(
  "Complete",
  Schema.Struct({ evidence: Schema.String }),
) {}

export const TokenIssueChart = Statechart.make(TokenIssue);

3 · Publish only after settlement succeeds

The durable outcome contains the key—not the material.

The Activity returns process-only-token-material. TokenEvidence.prepare stages it under an exact durable identity and returns evidence-1; settle then confirms Publish. Only that durable key crosses the outcome boundary into Complete.

import {
  Activity,
  Resource,
  State,
  Statechart,
  StatechartEngine,
  States,
  Transitions,
} from "@motive/motive";
import { Effect, Layer, Schema } from "effect";

export class Begin extends Schema.TaggedClass<Begin>()("Begin", {}) {}

export const TokenEvidence = Resource.make("TokenEvidence", {
  key: Schema.String,
  material: Schema.Struct({ rawToken: Schema.String }),
  resource: Schema.Struct({ claims: Schema.Array(Schema.String) }),
});
const EvidenceStore = TokenEvidence.as("EvidenceStore");

export const IssueToken = Activity.make("IssueToken", {
  success: Schema.Struct({ evidence: TokenEvidence.Output }),
});

export class TokenIssue extends State.Compound<TokenIssue>()("TokenIssue") {
  static states = States.make(() => [Idle, Issuing, Complete]);
}

class Idle extends State.Atomic<Idle>()("Idle") {
  static transitions = Transitions.make(this, ({ on }) => [on(Begin, Issuing)]);
}

class Issuing extends State.Atomic<Issuing>()("Issuing") {
  static transitions = Transitions.make(this, ({ invoke, on, provide }) => [
    provide(EvidenceStore),
    invoke(IssueToken),
    on(IssueToken.Done, Complete, ({ event }) => event.value),
  ]);
}

class Complete extends State.Done<Complete>()(
  "Complete",
  Schema.Struct({ evidence: Schema.String }),
) {}

export const TokenIssueChart = Statechart.make(TokenIssue);

export const makeLive = (record: (step: string) => void) =>
  TokenIssueChart.toLayer({
    activities: {
      IssueToken: () =>
        Effect.sync(() => record("activity")).pipe(
          Effect.andThen(Effect.sleep("500 millis")),
          Effect.as({ evidence: { rawToken: "process-only-token-material" } }),
        ),
    },
  }).pipe(
    Layer.provide(
      TokenEvidence.toLayer({
        prepare: ({ material, preparation }) =>
          Effect.sync(() => {
            record(
              `prepare:${material.rawToken}:${preparation.arming}-${preparation.attempt}`,
            );
            return "evidence-1";
          }),
        abandon: () => Effect.void,
        settle: () => Effect.void,
      }),
    ),
    Layer.provideMerge(StatechartEngine.layerMemory),
  );

Resource.Output is a Schema placement

A producing Resource owns three related Schemas:

export const TokenEvidence = Resource.make("TokenEvidence", {
  key: Schema.NonEmptyString,
  material: Schema.Struct({ rawToken: Schema.String }),
  resource: Schema.Struct({ claims: Schema.Array(Schema.String) }),
  preparationError: TokenPreparationFailed,
});

export const IssueToken = Activity.make("IssueToken", {
  success: Schema.Struct({ evidence: TokenEvidence.Output }),
});

material is the handler-side type at success.evidence. key is the durable type projected into the published IssueToken.Done event at the same field. resource is the value a later TokenEvidence.Input acquisition can receive.

The placement is part of the success Schema, not a post-processing convention. Motive can therefore find every output boundary, preserve its exact field path, and reject Resource.Output anywhere other than an Activity success contract.

prepare stages; settle publishes

provide(EvidenceStore) authors which occurrence owns the output. After the handler succeeds, the runner asks that provider to prepare the material under an exact durable identity:

prepare: ({ material, preparation }) =>
  evidenceStore.stage({
    bytes: material.rawToken,
    preparation,
  }),

abandon: ({ scope }) => evidenceStore.abandon(scope),

settle: ({ key, disposition, preparation }) =>
  disposition._tag === "Publish"
    ? evidenceStore.publish({ key, preparation })
    : Effect.void,

prepare returns the Resource key, but that return alone is not Activity success. Motive atomically commits the key-only success value, its exact finite output manifest, and a Publish intent. The Activity remains in Publishing until every provider confirms its intent. Only then does Motive construct IssueToken.Done.

Neither the raw token nor an open upload body enters the committed Snapshot or timeline. The published event contains only { evidence: "evidence-1" }, which another runner can serialize, compare, and later acquire.

The manifest is the point of no return

Before the manifest commits, failure, interruption, cancellation, or supersession has one legal disposition: abandon(scope). The scope identifies the exact provider, owner, Activity arming, and attempt, so the provider can remove every invisible stage even when only some output paths finished preparing.

After the manifest commits, abandonment is forbidden. Recovery repeats the same Publish intents with the same preparation identities until every provider confirms them. A lost acknowledgement may repeat settle(Publish); provider settlement therefore uses its durable preparation as the idempotency authority.

Multiple independent providers may become externally visible at different times. If several values must become visible atomically, model them as one composite Resource: that Resource is the canonical unit of external admission and settlement.

Preparation failure is not Activity success

Declare preparationError when staging can fail in a modeled way. Motive publishes neither a partial manifest nor a guessed key. It abandons the exact attempt scope and follows the typed Activity error path.

A defect in the provider remains a defect. Timeout, retry, and observability policy belong in the Effects implementing prepare, abandon, and settle, where the external boundary is explicit.

Test the protocol

  1. Return distinctive material and verify prepare receives it at the exact Output path.
  2. Block settle(Publish) and verify the Activity remains Publishing with no Done event.
  3. Confirm one output at a time and verify Done appears only after the final confirmation.
  4. Fail or interrupt before the manifest and verify one exact keyless abandon(scope).
  5. Restart after the manifest and verify the same Publish preparation is redriven and never abandoned.
  6. Verify committed Snapshots and timelines contain durable keys but no process-local material.

Next, Providers and Requirement Closure moves from one output boundary to composition: how open Resource requirements propagate through children until one authored provider dominates every possible active lifetime.