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
{ rawToken }Process-local · never committed
evidence-1Serializable · recoverable identity
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
- Return distinctive material and verify
preparereceives it at the exact Output path. - Block
settle(Publish)and verify the Activity remainsPublishingwith noDoneevent. - Confirm one output at a time and verify
Doneappears only after the final confirmation. - Fail or interrupt before the manifest and verify one exact keyless
abandon(scope). - Restart after the manifest and verify the same
Publishpreparation is redriven and never abandoned. - 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.