Skip to main content
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.