Two booleans, because there are two questions
A new project cannot answer anything and cannot index anything, because it has no models. That is correct and it is also the least helpful possible starting state, so the interesting design question is what it tells you about itself.

The first version was a boolean called ready. It was wrong in a specific way that took a while to see.
curl -s "https://oberik.com/api/projects/$ID/readiness" -H "X-API-Key: $KEY"
# -> { "canAnswer": true, "ready": true, "next": null, "steps": [ … ] }Two booleans now. Gate a deploy on canAnswer — it is true once the project can run a turn at all. ready is the full checklist, and it includes steps that only hold back one capability.
Here is the case that forced the split: turning on subagents without choosing subagent models makes ready false on a project that answers questions perfectly well. Gate your deploy on that single boolean and you have blocked a release over a feature nobody was using yet.
Every step carries essential if you want to make the distinction yourself, and each unfinished one names what it blocks and the one call that fixes it.
What ends up on the non-essential list
The pattern is consistent, and it is worth internalising because it is how you should think about your own health checks: essential means the project cannot answer at all. Everything else is a degradation, and a degradation must not fail a deploy.
A dead reranker. A rerank model that has stopped working costs a project the precision half of retrieval and nothing else. canAnswer stays true. ready goes false, and the step names both the reason and how to stop the setting claiming something that is not happening.
Worth knowing why that gets re-checked at all: a rerank that fails degrades to vector order on purpose, because losing the whole query because a cross-encoder is down would be worse. Which means a model that can never work looks exactly like one that works. So readiness asks the model to rerank two documents and reports what it said.
No embedding model, for memory. Memory, recall and the wiki all store and find text as vectors, so a project without an embedder cannot remember anything. But it answers questions fine — so calling this essential would fail the deploy of a working project. It still leaves ready: false and still shows up as next.
A scheduled task waiting on a person. tasks-waiting fires whenever any task's last run ended needs_input, naming the task, when it stopped, and the session holding the question. The project answers questions perfectly well; it is the scheduled work that stopped.
Your provider having a bad night. Once a streak of failed turns passes a handful, a turns step appears naming the count and the last reason. Not essential — a deploy gated on canAnswer is not held hostage to somebody else's outage.
Registering a model, which is the one thing that has to happen
curl -sX POST "https://oberik.com/api/projects/$ID/providers" \
-H "X-API-Key: $KEY" -H "content-type: application/json" \
-d '{"provider":"openai",
"models":["gpt-4o-mini","text-embedding-3-small"],
"values":{"api_key":"sk-…"}}'
# -> { …, "derived": { "defaultModel": "gpt-4o-mini",
# "embeddingModel": "text-embedding-3-small", "embeddingDim": 1536 } }derived is the rest of the setup done for you: the chat default, and the embedding model with its dimension measured rather than assumed.
That word is doing work. An embedding dimension that is wrong by assumption produces a vector store that accepts writes and returns nothing useful, and the symptom is "retrieval is bad" rather than "the dimension is wrong".
A project does not silently fall back to a shared model
Worth stating, because the alternative is what most platforms do and it seems friendlier.
If a project has no model configured, the turn fails with finish_reason: "no_model" and costs nothing. It does not quietly run on a platform model.
Three reasons, and only the first is philosophical. It would bill the operator rather than the project. It would land outside the spend cap that project set. And it would be attributed to nobody, which breaks per-customer cost accounting for everyone.
no_model is worth handling separately in your UI, because it is not the end-user's doing and not something they can act on. Showing it as a normal agent reply sends them looking for a different question to ask, when what is needed is for somebody else to finish setting the project up.
The other finish reasons
finish_reason is null when the agent finished on its own terms. Check it before treating a response as complete.
| what happened | who can fix it | |
|---|---|---|
max_tool_iterations | the tool-loop ceiling was hit | you, by raising it at mint time — or the project ceiling, which a mint can only narrow |
guardrail | an input guardrail refused the message | the end-user, by asking something else |
no_model | the project has no model configured | whoever set the project up |
The one to write UI copy for is the first, because of a detail that is easy to miss: content can be empty. If the ceiling lands mid-tool-loop — which is the common case, since that is what the ceiling counts — the agent had not started writing its reply yet.
if (res.finish_reason === "max_tool_iterations" && !res.content.trim()) {
show("That took more steps than allowed. Try a narrower question.");
}finish_reason is a machine-readable string, deliberately not a sentence. You decide how, and in which language, to tell the user.
There is no platform-imposed tool-loop limit
An agent that needs forty steps to finish a job gets forty. Bound it deliberately, at mint time:
{ subject: user.id, maxToolIterations: 12, models: ["openai/gpt-4o-mini"],
maxEffort: "low", expiresIn: 900 }With no project ceiling set there is no limit — an agent that has misread a task keeps calling tools until the spend cap stops it. A project serving end-users should set one.
Try it before you write any code
Every project has a Try agent button. It opens a chat client talking to your real project through this same API, on a disposable identity erased when you close it — so you can check your corpus, capabilities and models answer the way you expect before wiring anything up.
To see what a particular user sees, mint a token for them and press Try agent with this token. The same chat opens as that subject and scope: their documents, their memories, their scope boundary. Nothing is erased when you close that one, because none of it is yours.
Most of the integration bugs I have watched people hit would have shown up in five minutes there.
