Provisioning a real-world agent profile is not a synchronous call. Forming a legal
entity, passing KYB, registering an A2P/10DLC campaign, and propagating DNS take
hours to days. Naïve models this as a durable workflow so a signup never
blocks and a retried webhook never forms a second entity.
The state machine
provisioning → verifying → active
↘ needs_action (KYB doc, A2P rejection) → verifying
↘ awaiting_payment (card funding / formation checkout) → active
↘ failed → reconciled (orphans cancelled)
↘ revoked
Regulated steps run the real provider calls up to the payment step and then
stop: the card step calls Stripe Issuing and returns awaiting_payment with a
funding checkout_url in step.result (no charge, no issuance); formation returns
the $249 checkout; KYC starts a real hosted verification when members are
supplied. The agent never auto-spends — a human completes the checkout.
- Idempotent on the agent profile key — a retried signup webhook resumes the same
workflow; it never forms a second entity or issues a second card. Pass
idempotencyKey to provision().
- Async-native — formation and KYB take hours; A2P takes days; DNS needs
propagation. The workflow holds state across all of it and emits
agentProfile.ready / agentProfile.needs_action events.
- Self-healing — a reconciliation sweep cancels any capability-resource not
bound to an active agent profile within N minutes. The answer to “orphaned card” is
a janitor, not a saga engine.
It is implemented as a Postgres-backed durable workflow (idempotency keys + a
step ledger + a sweeper), not a Temporal cluster — so self-hosters get durable
provisioning without standing up Temporal + Cassandra.
Driving it from your app
const op = await naive.forUser(tenant.id).provision("sdr", {
idempotencyKey: `op:${tenant.id}`,
});
// op.status === "provisioning" — don't block; subscribe to events.
export async function onAgentProfileEvent(evt: NaiveEvent) {
if (evt.type === "agentProfile.ready")
await naive.runtime("pool").start(evt.data.agentProfileId, { goal: "Run outbound." });
if (evt.type === "agentProfile.needs_action")
/* surface the KYB doc / A2P step to the tenant in your dashboard */;
}
Poll status any time with await op.refresh() or GET /v1/agent-profiles/:id.
provision() runs this workflow for real: it plans a Postgres step ledger from
the template, advances each step, and derives the agent profile status. Steps that are
regulated/async by nature (KYB, LLC formation, A2P phone) or that need an
unconfigured provider resolve to needs_action with a precise requirement (read
them from agentProfile.steps); fully-automatable steps (e.g. an email inbox when a
verified domain + provider exist) execute immediately. The internal cron
POST /v1/cron/reconcile-agent profiles re-advances unfinished agentProfiles.
Why durable execution earns its keep here
This is the one place durable execution is load-bearing — and it is not atomic
rollback. It’s the multi-hour reality of regulated onboarding (the source of
today’s “stuck verification” friction): entity formation, KYB review, A2P/10DLC
registration, DNS propagation. The workflow is scoped to provisioning, not
general agent orchestration.