- engineering
How we ship a feature to the AI on the same day
There is no AI backlog. When the action ships, the AI can call it. Here's the contract every Odexy PR satisfies.
If you ask a typical SaaS team when their newly-shipped feature will be available to their AI assistant, the honest answer is "we'll see when the AI roadmap gets to it." That's the disconnect we designed out of Odexy.
Here's the contract every action PR in our codebase has to satisfy. If it's all green, the feature is shipped — to humans AND to the AI.
The contract
1. Zod input schema — defines the action's input shape. Single source of truth.
2. Zod output schema — defines the action's output shape.
3. Policy — who can call this? Imported from modules/<m>/policies/, not inline.
4. Handler — pure function. Reads via ctx.db, emits via ctx.events, logs via ctx.logger. No HTTP, no shell, no filesystem.
5. Description — 1-3 sentences. Written for an AI reading it. Mentions side effects + idempotency + dangerous-ness.
6. At least one @example — input + output, validated against the schema.
7. Test — happy path, policy-denial branch, validation failure.
8. Registered in the module's action index.
9. dangerous: true if destructive or sends anything externally.
Skip any step → PR gets rejected. There's a Biome lint rule (noRestrictedImports) that catches three of the most common drifts at build time — like importing nodemailer outside the email module.
What this looks like in practice
A Tuesday in March: an engineer ships expenses.expense.batch_approve. The PR adds a Zod schema, a policy ("expenses:expense:approve:team or :any"), a handler that walks the candidate expenses and runs the existing expense.approve for each (re-using the same audit-log + dispatch chain), a description ("Approves a batch of expenses in one transaction. Side effect: posts to Accounting per approval. Idempotent if called twice with the same expense IDs within 60 seconds."), and tests.
Wednesday morning, the AI calls it:
> "Approve all expenses under $50 in categories I've approved 5+ times before."
The AI agent:
1. Reads ctx.actor.permissions — does the requester have expenses:expense:approve:team?
2. Runs expenses.expense.list (read-only) to find candidates.
3. Filters by category history (this is the AI's reasoning).
4. Calls expenses.expense.batch_approve with the IDs.
5. Logs the call in audit_log with actor_type='ai', the user as actor_id.
No new code on the AI side. No prompt template change. No tool registration. The action was registered in the registry; the registry IS the tool list.
The thing this prevents
Every team that ships AI as a bolt-on hits the same wall: the AI knows about 60% of the product surface. Why? Because somebody had to enumerate "AI-callable features," and that list lagged the product roadmap. The 40% gap was full of new features, edge cases, and the operations that mattered most for power users.
The action layer collapses this: the AI's tool list IS the product's action registry. There is no gap.
What it costs
This contract is a discipline. Writing a Zod schema before the handler is fast; writing a policy file before the handler feels slow when you're iterating. We've learned to write all four — schema, policy, handler, description — in the same sitting. The PR template enforces it.
The other cost: occasionally we want to expose an internal helper as an AI tool without going through the full ceremony. We don't. The cost of "private AI tools" is permission drift — we've held the line on this and haven't regretted it.
If you want to copy this
The whole pattern lives in packages/actions/ in the Odexy repo. Open-sourcing the framework is on the 2027 roadmap; until then, the shape is documented in our AI architecture page.
— The Odexy team
