@plantagoai/flows
XState v5 state machine factory with Firestore persistence and pre-built flow presets. Define typed state machines, persist state across page reloads, and use ready-made patterns for common flows.
Consumers: Foundation (extracted), MarketHub
Installation
"@plantagoai/flows": "file:../../shared/packages/flows"
Peer Dependencies
| Dependency | Required For |
|---|---|
xstate (v5+) |
State machine engine (required) |
@xstate/react |
useFlow() React hook |
firebase-admin |
Server-side persistence |
firebase |
Client-side persistence |
react |
React hook |
Defining Flows
import { defineFlow } from "@plantagoai/flows";
const checkoutFlow = defineFlow({
id: "checkout",
initial: "cart",
persist: true, // save state to Firestore
context: { items: [], total: 0 },
states: {
cart: { on: { PROCEED: "shipping" } },
shipping: { on: { CONFIRM_ADDRESS: "payment", BACK: "cart" } },
payment: { on: { PAY: "processing", BACK: "shipping" } },
processing: { on: { SUCCESS: "confirmed", FAILURE: "failed" } },
confirmed: { type: "final" },
failed: { on: { RETRY: "payment" } },
},
});
FlowConfig
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Unique flow identifier |
initial |
string |
Yes | Initial state name |
states |
Record<string, FlowState> |
Yes | State definitions |
persist |
boolean |
No | Enable Firestore persistence |
context |
TContext |
No | Initial context data |
FlowState
interface FlowState {
on?: Record<string, string | FlowTransition>; // event -> target state
type?: "final" | "atomic"; // final = terminal state
meta?: Record<string, unknown>; // arbitrary metadata
}
interface FlowTransition {
target: string;
guard?: string; // guard condition name
}
FlowDef
The return value of defineFlow() includes a toMachine() method that creates an XState v5 machine:
const machine = checkoutFlow.toMachine();
// Standard XState machine — can be used with createActor(), interpret(), etc.
React Hook: useFlow
Manages the XState actor and Firestore persistence in one hook:
import { useFlow } from "@plantagoai/flows";
function CheckoutPage({ orderId }: { orderId: string }) {
const { state, send, can, matches, ready } = useFlow(checkoutFlow, orderId);
if (!ready) return <Spinner />;
return (
<div>
<p>Current step: {state}</p>
{matches("cart") && (
<button onClick={() => send({ type: "PROCEED" })}>
Proceed to Shipping
</button>
)}
{matches("shipping") && (
<>
<button onClick={() => send({ type: "BACK" })}>Back</button>
<button onClick={() => send({ type: "CONFIRM_ADDRESS" })}>
Continue to Payment
</button>
</>
)}
{matches("payment") && (
<button
onClick={() => send({ type: "PAY" })}
disabled={!can("PAY")}
>
Pay Now
</button>
)}
{matches("confirmed") && <p>Order confirmed!</p>}
</div>
);
}
UseFlowReturn
| Field | Type | Description |
|---|---|---|
state |
string |
Current state name |
send |
(event: FlowEvent) => void |
Send an event to the machine |
can |
(eventType: string) => boolean |
Check if an event is valid in current state |
matches |
(stateName: string) => boolean |
Check if currently in a state |
ready |
boolean |
true once snapshot is loaded from Firestore |
Persistence
State snapshots are saved to and restored from Firestore automatically when persist: true. Can also be used directly:
import { saveSnapshot, loadSnapshot, deleteSnapshot } from "@plantagoai/flows";
// Save
await saveSnapshot("checkout", "order-123", actorSnapshot);
// Load
const snapshot = await loadSnapshot("checkout", "order-123");
// Delete (e.g., after flow completes)
await deleteSnapshot("checkout", "order-123");
Snapshots are stored in Firestore at flow_snapshots/{flowId}_{instanceId}:
{
"flowId": "checkout",
"instanceId": "order-123",
"snapshot": { /* XState snapshot */ },
"updatedAt": "2026-04-16T10:00:00.000Z"
}
Presets
Ready-made flow definitions for common patterns. Each preset returns a FlowDef that can be customized.
Checkout Flow
cart → shipping? → payment → processing → confirmed | failed
import { checkoutFlow } from "@plantagoai/flows";
const flow = checkoutFlow({
requireShipping: true, // include shipping step (default: true)
paymentMethods: ["card", "apple_pay", "google_pay"],
persist: true,
});
Onboarding Flow
signup → verify_email? → profile → tos_accept? → welcome
import { onboardingFlow } from "@plantagoai/flows";
const flow = onboardingFlow({
requireEmailVerification: true, // include email verification step
requireTos: true, // include ToS acceptance step
persist: true,
});
Approval Flow
draft → submitted → review? → approved | rejected
import { approvalFlow } from "@plantagoai/flows";
const flow = approvalFlow({
requireReview: true, // include review step before approval
allowRevision: true, // rejected items can be revised and resubmitted
persist: true,
});
Governance Flow
Extracted from Foundation's multi-round governance system:
support_gathering → review? → voting → passed | rejected | discarded
import { governanceFlow } from "@plantagoai/flows";
const flow = governanceFlow({
supportThreshold: 100, // signatures needed to move to voting
votingDurationHours: 168, // 7 days
requireReview: true, // admin review between support and voting
persist: true,
});
Integration: Foundation Governance
Foundation's governance proposals use the flows package for multi-round state management:
import { governanceFlow, useFlow } from "@plantagoai/flows";
const govFlow = governanceFlow({ supportThreshold: 50, requireReview: true });
function ProposalPage({ proposalId }) {
const { state, send, matches } = useFlow(govFlow, proposalId);
if (matches("support_gathering")) return <SupportPhase onSign={() => send({ type: "SIGN" })} />;
if (matches("voting")) return <VotingPhase onVote={(opt) => send({ type: "VOTE", option: opt })} />;
if (matches("passed")) return <PassedBanner />;
}