Our family's restaurant, Wok & Karahi in Spring, Texas, runs on one AI system. I built the system behind it. It answers the phone, takes orders at the tables, runs the website and the ordering pages, pulls in the delivery apps, feeds the kitchen screen, takes every payment before food is cooked, and gives the family an app to run all of it. We run the technology. The team runs the room.
This post is about the part I find most interesting as an engineer: the job engine. Someone types one line into the app, like "the lava cake is gone until Friday". The system writes a plan, tags each step with a risk class, waits for one approval, runs the steps, and reports what it did, what it assumed and what it could not do. Every model call and every step goes into a ledger with its cost.
The surfaces
Guests reach us through six doors: the phone, a QR code on each table, the website, DoorDash, Uber Eats and Grubhub. Every door lands in one queue in Clover, our point-of-sale system. Each ticket carries the table number, the guest's own words and a pickup time computed from what the kitchen has in hand.
The AI host is called Haniya. She is the same host on the phone, at the table and on the website. After close she still answers, schedules the order for opening and texts a pay link. The ticket prints when the payment clears.
From the restaurant's own data, re-checked in October 2026: 100% of direct orders arrive with no staff member taking them. In mid-August it was 17%. More than 1 in 4 calls come in while we are closed, and every one is answered. Every app order is paid before it reaches the kitchen. The phone costs about $0.40 of AI per answered call, and about 1 in 3 callers takes the add-on.
The shape of it
A server on Fly.io runs Haniya, the ordering pages and the small job steps on a 15-second tick. Heavy steps, like driving a browser or editing the code repository, go to a second machine through a queue.
One record per capability
What Haniya could do used to live in five lists kept in sync by hand: the app's home list, the model's tools, the prompt text and the self-check among them. They drifted. Now each capability is one declarative record: an id, a title in plain words, an example, the tool schema, a risk class, the credentials it needs, whether it is ready, and a probe that checks it still works. The lists are generated from the records, and a test fails if one is written by hand.
Capabilities are atoms: read something, write something, post somewhere, generate, schedule, text, call, browse, edit code. The planner composes them. The bar for shipping the engine was that new kinds of asks run as jobs with no job-specific code.
Plan, gate, run, report
A job is planned, running, waiting, paused, done or failed. Each step is one capability call with its arguments, a risk class, a status, its cost, a short result, and an undo where the channel allows one. The risk class is set once per capability, in its record. The model never decides it per prompt.
- READ - sales, guests, calls. No gate.
- WRITE_INTERNAL - mark a dish sold out, add a register item, write a description. Runs, with an undo window and one line of why.
- OUTWARD - a social post, a text to a guest. Runs once the plan is approved. The first run of any new capability goes to a test account or a draft.
- MONEY_OR_FOOD - a refund, a free dish, a discount. A human tap, every time.
- DESTRUCTIVE - delete an item, block a number, cancel a booking. A human tap every time, with no "always allow".
The plan is shown once with three buttons: Do it, Edit, I'll do it. After Do it, the outward steps run with no more taps. Money, food and destructive steps stay inside the job as an amber "needs you" step. Haniya never gives money or food away on her own.
Take the lava cake. The plan marks it sold out everywhere, tells the chef, pulls it from this week's posts, puts it back on Friday at 11 AM, and confirms it is back. That is one ask with five steps, two of them scheduled.
Every job ends with a report in three lists: Done, Assumed, Could not do. If the planner guessed a price band or picked a channel on its own, it says so under Assumed.
When a capability is missing
A step can be build_capability or get_access. If a plan needs something the registry does not have, the job writes the code in a workbench, runs the tests and registers it. If it needs access to a platform, it files the application itself and uses a logged-in browser until the API is granted.
The "new dish" job shows the full shape. The input is a name and a reference photo. The plan reads pairings and a price band. It generates three images in our house style, and a vision model checks that the food matches the reference. It creates the register item and its options, then sets the online price and the delivery-app menus through a browser, because no API reaches them. It refreshes the website and phone menus, posts to Google, Facebook, Instagram and TikTok, and texts opted-in guests who ordered the closest dishes in the last 90 days. Then it repeats the posts on a launch schedule and stops early if the dish runs out.
The ledger
Every model call goes through one wrapper, and a test fails if any other file calls the model API directly. The wrapper routes the model: Haiku for reads and classification, Sonnet for writing, Opus never by default. It applies prompt caching, caps output tokens per call type and writes a ledger row.
The ledger is one JSONL file per job, with one line per model call and per tool step. It uses the OpenTelemetry GenAI field names and records all four token counts, cost from a price table, latency, a prompt hash and the outcome.
Each job is capped at ten times the median for its kind, with a $1 floor. A soft alert fires at 80% of the daily budget, and jobs that are not essential stop at 100%. Guest calls are never cut. Alerts also fire when a job costs more than three times its median, when a prompt over 512 tokens gets no cache reads, and when a call retries more than twice.
Six levels
The design doc sets six levels. Every slice of work has to move all six.
- L1, outcomes: you say what you want in one line, never which tool or channel.
- L2, any outcome: a new kind of ask is planned the same way as a known one.
- L3, self-extension: missing capabilities and access get built by steps.
- L4, initiative: watchers propose jobs from sales, reviews, alerts and calls.
- L5, learning: edits, yes and no, and outcomes become rules and better plans.
- L6, thinking out loud: every job says what it will do, why, what it costs and what it is unsure of.
L4 is live in a first form. Three watchers look at greeting hang-ups as a share of calls, phone orders over four days against the four before, and jobs that finished with gaps a re-run can fix. Each idea shows on the Today screen with two buttons, Plan it and Not now. Not now hides it for a week.
Above the engine sits an always-on operator: Claude Code on our own server, one beat every two hours, under a written rulebook. Bug fixes ship alone after tests. Prices, menu, deals, payments and anything a guest sees for the first time wait for a yes by text. Deletes and force-pushes are never allowed. A policy hook checks every tool call.
What it cannot do yet
- L5 is not built. Plans do not yet learn from outcomes.
- The full new-dish chain has not run end to end on a real dish. The browser steps for online prices and delivery apps, and the posting steps, shipped but have not been exercised live.
- Google refused our Business Profile API application twice, so Google posts go through a browser. Meta posting waits on a token. TikTok is drafts only.
- Our own delivery was switched on for everyone and had zero real paid orders as of mid-October.
- The speed numbers in the design doc, like a read answer under 4 seconds at the median and Approve under 1 second, are design targets.
What broke
On October 13, tests run in a git worktree with a real remote pushed test fixtures to main. They also rewrote the shared git identity and set the repository to bare. Now a push guard refuses any push to main with fixture authors or more than 200 deleted files. Repo tests never run in a checkout that can push.
On October 16, a backup machine ran beside the main one for five minutes. That risks doing the same work twice. The backup is now fenced: its queues are set aside at boot, its pollers are off, and it steps aside by itself after the main machine answers twice in a row.
We added a standby in a second region. It takes the public port after three straight minutes of a dead main machine. In the October 17 drill, the standby was serving four minutes after we cut off the main machine, and everything was handed back four minutes later.
A deploy to the phone server must never drop a call. A deploy request waits until there are no live calls and no live checkouts, checked twice 15 seconds apart, and it never deploys over a live call. If the ordering page cannot reach its server, guests get one button to our old hosted ordering page.
The first real job also found a bug in the engine itself. We fixed it.
I build the same kind of system for other independent restaurants through AZ Restaurant Partners.