Build the code | Claritty docs

Build the code

Every generated app is an ordinary project you can open and edit. Nothing is hidden behind the platform except hosting, credentials and the model proxy. Here is how one is put together.

The stack

Frontend
React, Vite and Tailwind. Your dashboard and your widgets.
Backend
Python and FastAPI with the Claritty SDK. Your agents, tools, workflows and triggers.
Data
PostgreSQL, provisioned and managed for you.
The manifest
intelligence.yaml at the root. Everything the app is allowed to do is declared there.

Project layout

intelligence.yaml     # what the app can do. The source of truth.
backend/
  agents/             # the parts that decide, one prompt per agent
  tools/              # the parts that act. Deterministic, no model call
  triggers/           # what starts a workflow
  routes/app.py       # your API, including the required GET /api/widget
  models.py           # your tables. Every row belongs to a user
  main.py
frontend/
  src/components/     # your Widget lives here
  src/pages/          # your dashboard
  src/theme.css       # colours and font

Tools act, agents decide

This is the line that matters most, and the one people get wrong. A tool is a callable action with no model call in it: read a table, send an email, write a row. An agent is a prompt that chooses which tools to call. Business logic belongs in a tool, where it is deterministic and testable, not in an agent’s prompt where it is re-decided on every run.

intelligence.yaml
tools:
  - id: app.list_overdue
    handler: backend.tools.list_overdue:run
    output:
      invoices: { type: array, required: true }

agents:
  - id: chaser
    model: claude-sonnet-4-6
    tools: [app.list_overdue, gmail.send]
    promptFile: backend/agents/chaser.md

workflows:
  - id: chase
    steps:
      - id: run
        agent: chaser
An agent can only call tools listed in its own tools. This is not advisory. A prompt that talks about an unlisted tool produces an agent that cannot call it, and the manifest is rejected if the tool does not exist at all.

The widget endpoint

The dashboard polls one endpoint for widget data, roughly every 30 seconds. Every app has to provide it:

GET /api/widget
{
  "title": "Today",
  "value": "12 new",
  "items": [ ... ]
}

Return whatever your widget renders. Keep the shape and your Widget.tsx in step with each other. An empty widget on the dashboard is almost always this endpoint returning something the component did not expect.

Calling the model

Agents reach the model through the SDK, never a raw provider key. The proxy handles routing and billing.

python
from claritty_sdk.llm import get_llm_client

client = get_llm_client("claude-sonnet-4-6")
result = client.chat(
    [{"role": "user", "content": "Summarise today's mentions"}],
    system="You are terse. Answer in one sentence.",
)
print(result.content, result.usage.total_tokens)
get_llm_client(model)The model id is required. The proxy routes to the right provider from it.
chat(messages, …)Takes a list of message dicts, not a string. Returns a ChatResult with content, finish_reason and usage.
chat_with_tools(…)Same, for a tool-calling loop.
chat_stream(…)Same, streamed.
Give an agent a deterministic fallback so the app still does something sensible when the model is unavailable. Generated apps ship with one.

Running locally

You do not need a provider key. With no token set, agents use their fallback, so a fresh clone runs with no configuration. The only thing it needs is DATABASE_URL, which Docker Compose provides.

For real model calls while developing, run claritty login and the app is wired to the managed proxy. Still no provider key on your side.

One app, many users

Apps are multi-tenant. Every row belongs to a user, and every request carries an X-User-ID header.

python
# Wrong. Returns every user's rows.
invoices = session.query(Invoice).all()

# Right.
invoices = session.query(Invoice).filter(Invoice.user_id == user_id).all()
A query missing its user filter is the single most common reason a deploy is refused, and the only bug on this page that leaks one customer’s data to another. Filter every query.

Verifying before you ship

shell
claritty test              # runs the app for real, locally
claritty deploy --dry-run  # runs the gates, changes nothing