# Expense Tracker (/docs/receipes/expense-tracker)



This recipe tells an agent exactly how to build an expense tracker in andibase.

Goal [#goal]

Create three things in one workspace:

1. A `budget` data definition.
2. An `expense` data definition.
3. One agent that helps users manage budgets and log expenses.
4. One mini app that shows budget progress and recent expenses.

Expected outcome [#expected-outcome]

After following this recipe, the workspace should have:

* a `budget` definition for monthly or custom budgets
* an `expense` definition for individual spending entries
* an `expense-tracker-agent` agent
* an `expense-tracker` mini app

Use the API documented in:

* [Data Model](/docs/data-model)
* [Apps](/docs/apps)
* [Get started (for AI Agents)](/docs/agent-get-started)
* [Data definitions API](/docs/api-reference/data-definitions)
* [Data API](/docs/api-reference/data)
* [Agents API](/docs/api-reference/agents)
* [Apps API reference](/docs/api-reference/apps)

Agent instructions [#agent-instructions]

When an agent executes this recipe, it should follow these rules:

1. Create the `budget` definition first.
2. Create the `expense` definition second.
3. In the current API, data definition handles are generated from `name`. Use `name: "Budget"` and `name: "Expense"` so the created handles normalize to `budget` and `expense`.
4. If your execution environment can resolve the created budget definition id, wire the expense link as a `relationship` field. If it cannot, use a text foreign key such as `budgetRecordId` instead, because the public HTTP API still expects `dataDefinitionId` for relationship fields.
5. Create the expense-tracker agent with instructions focused on budget management and expense logging.
6. Create the mini app last.
7. Do not invent custom fields outside this recipe unless the user explicitly asks for them.
8. Use sensible defaults and ask the user only when a missing choice materially changes money behavior, such as currency or budget period.

Step 1: Create the data model [#step-1-create-the-data-model]

Create two data definitions: `budget` and `expense`.

1\. Budget definition [#1-budget-definition]

Use `name: "Budget"`. The current API derives the handle from the name, so this will create the `budget` handle.

Recommended fields:

```json
{
  "name": "Budget",
  "description": "Tracks a planned spending period and its limits.",
  "fields": {
    "periodName": {
      "name": "Period name",
      "type": "text"
    },
    "periodType": {
      "name": "Period type",
      "type": "select",
      "options": [
        { "value": "weekly", "label": "Weekly", "color": "sky" },
        { "value": "monthly", "label": "Monthly", "color": "blue" },
        { "value": "quarterly", "label": "Quarterly", "color": "violet" },
        { "value": "yearly", "label": "Yearly", "color": "emerald" },
        { "value": "custom", "label": "Custom", "color": "zinc" }
      ]
    },
    "startDate": {
      "name": "Start date",
      "type": "date"
    },
    "endDate": {
      "name": "End date",
      "type": "date"
    },
    "currency": {
      "name": "Currency",
      "type": "select",
      "options": [
        { "value": "CLP", "label": "CLP", "color": "red" },
        { "value": "USD", "label": "USD", "color": "green" },
        { "value": "EUR", "label": "EUR", "color": "blue" },
        { "value": "GBP", "label": "GBP", "color": "violet" }
      ]
    },
    "totalBudget": {
      "name": "Total budget",
      "type": "number"
    },
    "spentAmount": {
      "name": "Spent amount",
      "type": "number"
    },
    "remainingAmount": {
      "name": "Remaining amount",
      "type": "number"
    },
    "status": {
      "name": "Status",
      "type": "select",
      "options": [
        { "value": "draft", "label": "Draft", "color": "zinc" },
        { "value": "active", "label": "Active", "color": "green" },
        { "value": "paused", "label": "Paused", "color": "amber" },
        { "value": "closed", "label": "Closed", "color": "blue" },
        { "value": "archived", "label": "Archived", "color": "stone" }
      ]
    },
    "categoryBudgets": {
      "name": "Category budgets",
      "description": "Optional JSON map of category to planned amount.",
      "type": "json"
    },
    "notes": {
      "name": "Notes",
      "type": "text",
      "variant": "long-text"
    }
  }
}
```

Recommended defaults for agent-created budget rows:

* `periodType`: `monthly`
* `currency`: use the workspace default or ask once
* `spentAmount`: `0`
* `remainingAmount`: same value as `totalBudget`
* `status`: `active`

2\. Expense definition [#2-expense-definition]

Use `name: "Expense"`. The current API derives the handle from the name, so this will create the `expense` handle.

Recommended fields:

```json
{
  "name": "Expense",
  "description": "Tracks one spending event or charge.",
  "fields": {
    "title": {
      "name": "Title",
      "type": "text"
    },
    "description": {
      "name": "Description",
      "type": "text",
      "variant": "long-text"
    },
    "amount": {
      "name": "Amount",
      "type": "number"
    },
    "currency": {
      "name": "Currency",
      "type": "select",
      "options": [
        { "value": "CLP", "label": "CLP", "color": "red" },
        { "value": "USD", "label": "USD", "color": "green" },
        { "value": "EUR", "label": "EUR", "color": "blue" },
        { "value": "GBP", "label": "GBP", "color": "violet" }
      ]
    },
    "expenseDate": {
      "name": "Expense date",
      "type": "date"
    },
    "category": {
      "name": "Category",
      "type": "select",
      "options": [
        { "value": "housing", "label": "Housing", "color": "stone" },
        { "value": "groceries", "label": "Groceries", "color": "green" },
        { "value": "transport", "label": "Transport", "color": "sky" },
        { "value": "utilities", "label": "Utilities", "color": "yellow" },
        { "value": "health", "label": "Health", "color": "red" },
        { "value": "education", "label": "Education", "color": "blue" },
        { "value": "shopping", "label": "Shopping", "color": "pink" },
        { "value": "dining", "label": "Dining", "color": "orange" },
        {
          "value": "entertainment",
          "label": "Entertainment",
          "color": "violet"
        },
        { "value": "travel", "label": "Travel", "color": "cyan" },
        {
          "value": "subscriptions",
          "label": "Subscriptions",
          "color": "indigo"
        },
        { "value": "debt", "label": "Debt", "color": "rose" },
        { "value": "taxes", "label": "Taxes", "color": "amber" },
        { "value": "gifts", "label": "Gifts", "color": "fuchsia" },
        { "value": "other", "label": "Other", "color": "zinc" }
      ]
    },
    "paymentMethod": {
      "name": "Payment method",
      "type": "select",
      "options": [
        { "value": "cash", "label": "Cash", "color": "zinc" },
        { "value": "debit-card", "label": "Debit card", "color": "blue" },
        { "value": "credit-card", "label": "Credit card", "color": "violet" },
        {
          "value": "bank-transfer",
          "label": "Bank transfer",
          "color": "emerald"
        },
        {
          "value": "digital-wallet",
          "label": "Digital wallet",
          "color": "cyan"
        },
        { "value": "other", "label": "Other", "color": "stone" }
      ]
    },
    "status": {
      "name": "Status",
      "type": "select",
      "options": [
        { "value": "pending", "label": "Pending", "color": "amber" },
        { "value": "cleared", "label": "Cleared", "color": "green" },
        { "value": "reimbursable", "label": "Reimbursable", "color": "blue" },
        { "value": "reimbursed", "label": "Reimbursed", "color": "cyan" },
        { "value": "cancelled", "label": "Cancelled", "color": "stone" }
      ]
    },
    "merchant": {
      "name": "Merchant",
      "type": "text"
    },
    "isRecurring": {
      "name": "Is recurring",
      "type": "boolean"
    },
    "recurringCadence": {
      "name": "Recurring cadence",
      "type": "select",
      "options": [
        { "value": "one-time", "label": "One time", "color": "zinc" },
        { "value": "weekly", "label": "Weekly", "color": "sky" },
        { "value": "monthly", "label": "Monthly", "color": "blue" },
        { "value": "yearly", "label": "Yearly", "color": "emerald" }
      ]
    },
    "receipt": {
      "name": "Receipt",
      "type": "files"
    },
    "tags": {
      "name": "Tags",
      "type": "multi-select",
      "options": [
        { "value": "personal", "label": "Personal", "color": "blue" },
        { "value": "business", "label": "Business", "color": "violet" },
        { "value": "family", "label": "Family", "color": "green" },
        { "value": "urgent", "label": "Urgent", "color": "red" }
      ]
    },
    "budgetRecordId": {
      "name": "Budget record id",
      "description": "Store the linked budget row id when the client only has public HTTP API access.",
      "type": "text"
    }
  }
}
```

If your client already has the created budget definition id, you can replace `budgetRecordId` with:

```json
{
  "budgetId": {
    "name": "Budget",
    "type": "relationship",
    "dataDefinitionId": "<budget-definition-id>"
  }
}
```

3\. Example create requests [#3-example-create-requests]

Create the definitions with `POST /api/v1/data-definitions`.

Current API note:

* the create payload uses `name`, `description`, and `fields`
* the handle is derived from `name`
* data rows are created with `data`, not `attributes`

Example for `budget`:

```bash
curl -X POST "https://andibase.com/api/v1/data-definitions" \
  -H "Authorization: Bearer $ANDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Budget",
    "description": "Tracks a planned spending period and its limits.",
    "fields": {
      "periodName": { "name": "Period name", "type": "text" },
      "periodType": {
        "name": "Period type",
        "type": "select",
        "options": [
          { "value": "monthly", "label": "Monthly", "color": "blue" },
          { "value": "custom", "label": "Custom", "color": "zinc" }
        ]
      },
      "startDate": { "name": "Start date", "type": "date" },
      "endDate": { "name": "End date", "type": "date" },
      "currency": {
        "name": "Currency",
        "type": "select",
        "options": [
          { "value": "CLP", "label": "CLP", "color": "red" },
          { "value": "USD", "label": "USD", "color": "green" }
        ]
      },
      "totalBudget": { "name": "Total budget", "type": "number" },
      "spentAmount": { "name": "Spent amount", "type": "number" },
      "remainingAmount": { "name": "Remaining amount", "type": "number" },
      "status": {
        "name": "Status",
        "type": "select",
        "options": [
          { "value": "active", "label": "Active", "color": "green" },
          { "value": "closed", "label": "Closed", "color": "blue" }
        ]
      }
    }
  }'
```

Create sample expense rows with `POST /api/v1/data-definitions/expense/data/upsert-many`.

```bash
curl -X POST "https://andibase.com/api/v1/data-definitions/expense/data/upsert-many" \
  -H "Authorization: Bearer $ANDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "data": {
          "title": "Supermarket",
          "amount": 42500,
          "currency": "CLP",
          "expenseDate": "2026-03-12",
          "category": "groceries",
          "paymentMethod": "debit-card",
          "status": "cleared",
          "merchant": "Jumbo",
          "isRecurring": false,
          "recurringCadence": "one-time",
          "budgetRecordId": "replace-with-created-budget-row-id"
        }
      }
    ]
  }'
```

Step 2: Create the budget assistant agent [#step-2-create-the-budget-assistant-agent]

Create one workspace agent with handle `expense-tracker-agent`.

Recommended behavior:

* help users create a budget for a period
* update an existing budget when the user changes limits or dates
* add expenses one by one from natural language
* suggest the closest matching category when the user does not provide one
* warn when an expense would push the active budget over its limit
* keep `spentAmount` and `remainingAmount` on the linked budget in sync after each expense change

Recommended agent payload:

```json
{
  "name": "Expense Tracker Agent",
  "handle": "expense-tracker-agent",
  "description": "Creates budgets, logs expenses, and keeps budget totals updated.",
  "model": "openai/gpt-5.4",
  "capabilities": {
    "webAccess": false,
    "browserAccess": false,
    "objectsAccess": true
  },
  "instructions": "You manage a workspace expense tracker. Use the budget and expense data definitions as the source of truth. Be action-oriented: make reasonable assumptions, complete the work when the intended outcome is clear, and avoid asking for confirmation on every small step. When the user asks to create a budget, create one budget row with a clear periodName, startDate, endDate, currency, totalBudget, spentAmount, remainingAmount, and status. When the user adds an expense, create an expense row, attach it to the correct active budget when possible, then update the linked budget spentAmount and remainingAmount. If category is missing, infer the best category from the title or merchant and say which category you used. If currency is missing, use the active budget currency. Never create duplicate budgets for the same period unless the user explicitly asks for a separate budget. If multiple active budgets could match, ask one short clarifying question. If no budget exists, create one first when the intended budget is obvious; otherwise ask one focused question."
}
```

Create the agent with `POST /api/v1/agents`.

Step 3: Create the mini app [#step-3-create-the-mini-app]

Create one app with handle `expense-tracker`.

Mini app requirements:

* read budgets and expenses using `window.andibase.fetch`
* show the active budget summary at the top
* show total budget, spent amount, remaining amount, and percent used
* show recent expenses in a simple table
* group expenses by category and show totals
* keep styling simple and flat

Current API note:

* `POST /api/v1/apps` currently accepts `name`, `handle`, `description`, and `code`
* app access fields such as `memberRole` and `publicPermissions` are server-managed defaults on create
* new apps are created as member-only by default

Example app code:

```jsx
import { useEffect, useMemo, useState } from "react";

const currencyFormatters = {
  CLP: new Intl.NumberFormat("es-CL", {
    style: "currency",
    currency: "CLP",
    maximumFractionDigits: 0,
  }),
  USD: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }),
  EUR: new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" }),
  GBP: new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" }),
};

function formatMoney(value, currency) {
  const formatter = currencyFormatters[currency] ?? currencyFormatters.USD;
  return formatter.format(Number(value || 0));
}

async function fetchAllPages(path, pageSize = 100) {
  const items = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await window.andibase.fetch(
      `${path}?page=${page}&pageSize=${pageSize}`,
    );
    const payload = await response.json();

    items.push(...(payload.items ?? []));
    hasMore = Boolean(payload.hasMore);
    page += 1;
  }

  return items;
}

export default function App() {
  const [budgets, setBudgets] = useState([]);
  const [expenses, setExpenses] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;

    async function load() {
      setLoading(true);

      const [budgetItems, expenseItems] = await Promise.all([
        fetchAllPages("/data-definitions/budget/query", 20),
        fetchAllPages("/data-definitions/expense/query", 100),
      ]);

      if (cancelled) return;

      setBudgets(budgetItems);
      setExpenses(expenseItems);
      setLoading(false);
    }

    load().catch(() => setLoading(false));

    return () => {
      cancelled = true;
    };
  }, []);

  const activeBudget = useMemo(
    () =>
      budgets.find((item) => item.data?.status === "active") ??
      budgets[0] ??
      null,
    [budgets],
  );

  const currency = activeBudget?.data?.currency ?? "USD";
  const totalBudget = Number(activeBudget?.data?.totalBudget ?? 0);
  const spentAmount = Number(activeBudget?.data?.spentAmount ?? 0);
  const remainingAmount = Number(
    activeBudget?.data?.remainingAmount ?? totalBudget - spentAmount,
  );
  const percentUsed =
    totalBudget > 0 ? Math.min(100, (spentAmount / totalBudget) * 100) : 0;

  const categoryTotals = useMemo(() => {
    const totals = new Map();

    for (const item of expenses) {
      const category = item.data?.category ?? "other";
      const amount = Number(item.data?.amount ?? 0);
      totals.set(category, (totals.get(category) ?? 0) + amount);
    }

    return Array.from(totals.entries()).sort((a, b) => b[1] - a[1]);
  }, [expenses]);

  if (loading) {
    return (
      <main
        style={{ minHeight: "100vh", padding: 24, fontFamily: "system-ui" }}
      >
        Loading...
      </main>
    );
  }

  return (
    <main style={{ minHeight: "100vh", padding: 24, fontFamily: "system-ui" }}>
      <h1>Expense tracker</h1>
      <section>
        <h2>{activeBudget?.data?.periodName ?? "Current budget"}</h2>
        <p>Total: {formatMoney(totalBudget, currency)}</p>
        <p>Spent: {formatMoney(spentAmount, currency)}</p>
        <p>Remaining: {formatMoney(remainingAmount, currency)}</p>
        <p>Used: {percentUsed.toFixed(1)}%</p>
      </section>

      <section>
        <h2>By category</h2>
        <ul>
          {categoryTotals.map(([category, amount]) => (
            <li key={category}>
              {category}: {formatMoney(amount, currency)}
            </li>
          ))}
        </ul>
      </section>

      <section>
        <h2>Recent expenses</h2>
        <table>
          <thead>
            <tr>
              <th align="left">Date</th>
              <th align="left">Title</th>
              <th align="left">Category</th>
              <th align="right">Amount</th>
            </tr>
          </thead>
          <tbody>
            {expenses.map((item) => (
              <tr key={item.id}>
                <td>{item.data?.expenseDate ?? "-"}</td>
                <td>{item.data?.title ?? "-"}</td>
                <td>{item.data?.category ?? "-"}</td>
                <td align="right">
                  {formatMoney(
                    item.data?.amount,
                    item.data?.currency ?? currency,
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </section>
    </main>
  );
}
```

Create the app with `POST /api/v1/apps`.

Example create request:

```bash
curl -X POST "https://andibase.com/api/v1/apps" \
  -H "Authorization: Bearer $ANDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Expense Tracker",
    "handle": "expense-tracker",
    "description": "Shows budget progress and recent expenses.",
    "code": "import { useEffect, useMemo, useState } from \"react\";\\n\\nconst currencyFormatters = {\\n  CLP: new Intl.NumberFormat(\"es-CL\", { style: \"currency\", currency: \"CLP\", maximumFractionDigits: 0 }),\\n  USD: new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\" }),\\n  EUR: new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"EUR\" }),\\n  GBP: new Intl.NumberFormat(\"en-GB\", { style: \"currency\", currency: \"GBP\" }),\\n};\\n\\nfunction formatMoney(value, currency) {\\n  const formatter = currencyFormatters[currency] ?? currencyFormatters.USD;\\n  return formatter.format(Number(value || 0));\\n}\\n\\nasync function fetchAllPages(path, pageSize = 100) {\\n  const items = [];\\n  let page = 1;\\n  let hasMore = true;\\n\\n  while (hasMore) {\\n    const response = await window.andibase.fetch(`${path}?page=${page}&pageSize=${pageSize}`);\\n    const payload = await response.json();\\n\\n    items.push(...(payload.items ?? []));\\n    hasMore = Boolean(payload.hasMore);\\n    page += 1;\\n  }\\n\\n  return items;\\n}\\n\\nexport default function App() {\\n  const [budgets, setBudgets] = useState([]);\\n  const [expenses, setExpenses] = useState([]);\\n  const [loading, setLoading] = useState(true);\\n\\n  useEffect(() => {\\n    let cancelled = false;\\n\\n    async function load() {\\n      setLoading(true);\\n\\n      const [budgetItems, expenseItems] = await Promise.all([\\n        fetchAllPages(\"/data-definitions/budget/query\", 20),\\n        fetchAllPages(\"/data-definitions/expense/query\", 100),\\n      ]);\\n\\n      if (cancelled) return;\\n\\n      setBudgets(budgetItems);\\n      setExpenses(expenseItems);\\n      setLoading(false);\\n    }\\n\\n    load().catch(() => setLoading(false));\\n\\n    return () => {\\n      cancelled = true;\\n    };\\n  }, []);\\n\\n  const activeBudget = useMemo(() => budgets.find((item) => item.data?.status === \"active\") ?? budgets[0] ?? null, [budgets]);\\n\\n  const currency = activeBudget?.data?.currency ?? \"USD\";\\n  const totalBudget = Number(activeBudget?.data?.totalBudget ?? 0);\\n  const spentAmount = Number(activeBudget?.data?.spentAmount ?? 0);\\n  const remainingAmount = Number(activeBudget?.data?.remainingAmount ?? totalBudget - spentAmount);\\n  const percentUsed = totalBudget > 0 ? Math.min(100, (spentAmount / totalBudget) * 100) : 0;\\n\\n  const categoryTotals = useMemo(() => {\\n    const totals = new Map();\\n\\n    for (const item of expenses) {\\n      const category = item.data?.category ?? \"other\";\\n      const amount = Number(item.data?.amount ?? 0);\\n      totals.set(category, (totals.get(category) ?? 0) + amount);\\n    }\\n\\n    return Array.from(totals.entries()).sort((a, b) => b[1] - a[1]);\\n  }, [expenses]);\\n\\n  if (loading) {\\n    return <main style={{ minHeight: \"100vh\", padding: 24, fontFamily: \"system-ui\" }}>Loading...</main>;\\n  }\\n\\n  return (\\n    <main style={{ minHeight: \"100vh\", padding: 24, fontFamily: \"system-ui\" }}>\\n      <h1>Expense tracker</h1>\\n      <section>\\n        <h2>{activeBudget?.data?.periodName ?? \"Current budget\"}</h2>\\n        <p>Total: {formatMoney(totalBudget, currency)}</p>\\n        <p>Spent: {formatMoney(spentAmount, currency)}</p>\\n        <p>Remaining: {formatMoney(remainingAmount, currency)}</p>\\n        <p>Used: {percentUsed.toFixed(1)}%</p>\\n      </section>\\n\\n      <section>\\n        <h2>By category</h2>\\n        <ul>\\n          {categoryTotals.map(([category, amount]) => (\\n            <li key={category}>\\n              {category}: {formatMoney(amount, currency)}\\n            </li>\\n          ))}\\n        </ul>\\n      </section>\\n\\n      <section>\\n        <h2>Recent expenses</h2>\\n        <table>\\n          <thead>\\n            <tr>\\n              <th align=\"left\">Date</th>\\n              <th align=\"left\">Title</th>\\n              <th align=\"left\">Category</th>\\n              <th align=\"right\">Amount</th>\\n            </tr>\\n          </thead>\\n          <tbody>\\n            {expenses.map((item) => (\\n              <tr key={item.id}>\\n                <td>{item.data?.expenseDate ?? \"-\"}</td>\\n                <td>{item.data?.title ?? \"-\"}</td>\\n                <td>{item.data?.category ?? \"-\"}</td>\\n                <td align=\"right\">\\n                  {formatMoney(item.data?.amount, item.data?.currency ?? currency)}\\n                </td>\\n              </tr>\\n            ))}\\n          </tbody>\\n        </table>\\n      </section>\\n    </main>\\n  );\\n}\\n"
  }'
```

If the app later adds host-driven drawers for creating or editing expenses, refresh data with `useAndibaseRefreshOnReturn(loadData)` from `@andibase/hooks`. Do not attach unconditional `window.focus` refresh listeners inside the iframe, because the first click into the app will usually focus the iframe and can look like a full reload.

Recommended acceptance checks [#recommended-acceptance-checks]

An agent should consider the recipe complete only if all of the following are true:

1. `budget` exists and includes the required fields.
2. `expense` exists and includes category options plus either the `budgetId` relationship or the `budgetRecordId` fallback used by the current public HTTP API flow.
3. `expense-tracker-agent` exists.
4. `expense-tracker` exists.
5. At least one sample budget row can be created successfully.
6. At least one sample expense row can be created successfully.
7. The app loads and displays budget and expense data without runtime auth errors.

Minimal delivery summary [#minimal-delivery-summary]

When the agent finishes, it should report:

* the created data definition handles
* the created agent handle
* the created app handle
* whether the app is member-only
* any assumptions made for default currency or budget period


## Documentation Navigation
Use these paths to traverse the relevant docs and generated API reference files for the app.
- Expense Tracker [current] -> `/docs/receipes/expense-tracker` (source: `content/docs/receipes/expense-tracker.mdx`)
- andibase Overview -> `/docs` (source: `content/docs/index.mdx`)
- Get started (for AI Agents) -> `/docs/agent-get-started` (source: `content/docs/agent-get-started.mdx`)
- Agent Tools -> `/docs/agent-tools` (source: `content/docs/agent-tools.mdx`)
- Agents -> `/docs/agents` (source: `content/docs/agents.mdx`)
- Agent Auth -> `/docs/api-reference/agent-auth`
- Get agent login request -> `/docs/api-reference/agent-auth/get-api-v1-agent-auth-requests-usercode`
- Exchange agent login -> `/docs/api-reference/agent-auth/post-api-v1-agent-auth-exchange`
- Start agent login -> `/docs/api-reference/agent-auth/post-api-v1-agent-auth-requests`
- Approve agent login -> `/docs/api-reference/agent-auth/post-api-v1-agent-auth-requests-usercode-approve`
- Deny agent login -> `/docs/api-reference/agent-auth/post-api-v1-agent-auth-requests-usercode-deny`
- Agents -> `/docs/api-reference/agents`
- Delete workspace agent -> `/docs/api-reference/agents/delete-api-v1-agents-id`
- List workspace agents -> `/docs/api-reference/agents/get-api-v1-agents`
- Get workspace agent -> `/docs/api-reference/agents/get-api-v1-agents-id`
- List agent chats -> `/docs/api-reference/agents/get-api-v1-agents-id-chats`
- Get agent chat -> `/docs/api-reference/agents/get-api-v1-agents-id-chats-chatid`
- Update workspace agent -> `/docs/api-reference/agents/patch-api-v1-agents-id`
- Create workspace agent -> `/docs/api-reference/agents/post-api-v1-agents`
- Send agent message -> `/docs/api-reference/agents/post-api-v1-agents-id-chats-chatid-messages`
- Apps -> `/docs/api-reference/apps`
- Delete app -> `/docs/api-reference/apps/delete-api-v1-apps-id`
- List workspace apps -> `/docs/api-reference/apps/get-api-v1-apps`
- Get app by id -> `/docs/api-reference/apps/get-api-v1-apps-id`
- Update app -> `/docs/api-reference/apps/patch-api-v1-apps-id`
- Create app -> `/docs/api-reference/apps/post-api-v1-apps`
- Automations -> `/docs/api-reference/automations`
- Delete workspace automation -> `/docs/api-reference/automations/delete-api-v1-automations-id`
- List workspace automations -> `/docs/api-reference/automations/get-api-v1-automations`
- Get workspace automation -> `/docs/api-reference/automations/get-api-v1-automations-id`
- List automation runs -> `/docs/api-reference/automations/get-api-v1-automations-id-runs`
- Get automation run -> `/docs/api-reference/automations/get-api-v1-automations-id-runs-runid`
- Update workspace automation -> `/docs/api-reference/automations/patch-api-v1-automations-id`
- Create workspace automation -> `/docs/api-reference/automations/post-api-v1-automations`
- Run automation -> `/docs/api-reference/automations/post-api-v1-automations-id-run`
- Trigger automation webhook -> `/docs/api-reference/automations/post-api-v1-automations-webhooks-publicid-secret`
- Channels -> `/docs/api-reference/channels`
- Delete channel -> `/docs/api-reference/channels/delete-api-v1-channels-channelid`
- List channels -> `/docs/api-reference/channels/get-api-v1-channels`
- Get channel -> `/docs/api-reference/channels/get-api-v1-channels-channelid`
- Update channel -> `/docs/api-reference/channels/patch-api-v1-channels-channelid`
- Create channel -> `/docs/api-reference/channels/post-api-v1-channels`
- Data -> `/docs/api-reference/data`
- Data Definitions -> `/docs/api-reference/data-definitions`
- Delete data definition -> `/docs/api-reference/data-definitions/delete-api-v1-data-definitions-id`
- List data definitions -> `/docs/api-reference/data-definitions/get-api-v1-data-definitions`
- Get data definition -> `/docs/api-reference/data-definitions/get-api-v1-data-definitions-id`
- Update data definition -> `/docs/api-reference/data-definitions/patch-api-v1-data-definitions-id`
- Create data definition -> `/docs/api-reference/data-definitions/post-api-v1-data-definitions`
- Data SQL Query -> `/docs/api-reference/data-sql-query`
- Run SQL query against workspace data -> `/docs/api-reference/data-sql-query/post-api-v1-data-sql-query`
- Get data by id -> `/docs/api-reference/data/get-api-v1-data-definitions-definitionid-data-id`
- Select all data row ids -> `/docs/api-reference/data/get-api-v1-data-definitions-definitionid-data-select-all`
- List data -> `/docs/api-reference/data/get-api-v1-data-definitions-definitionid-query`
- Patch many data rows -> `/docs/api-reference/data/patch-api-v1-data-definitions-definitionid-data-patch-many`
- Delete many data rows -> `/docs/api-reference/data/post-api-v1-data-definitions-definitionid-data-delete-many`
- Upsert many data rows -> `/docs/api-reference/data/post-api-v1-data-definitions-definitionid-data-upsert-many`
- DuckDB Query -> `/docs/api-reference/duckdb-query`
- Run a DuckDB query against registered sources -> `/docs/api-reference/duckdb-query/post-api-v1-duckdb-query`
- Explorer -> `/docs/api-reference/explorer`
- Delete explorer folder -> `/docs/api-reference/explorer/delete-api-v1-workspace-nodes-nodeid`
- List explorer nodes -> `/docs/api-reference/explorer/get-api-v1-workspace-nodes`
- List explorer folders -> `/docs/api-reference/explorer/get-api-v1-workspace-nodes-folders`
- Rename explorer folder -> `/docs/api-reference/explorer/patch-api-v1-workspace-nodes-nodeid-rename`
- Create folder -> `/docs/api-reference/explorer/post-api-v1-workspace-nodes-folders`
- Move explorer node -> `/docs/api-reference/explorer/post-api-v1-workspace-nodes-nodeid-move`
- Files -> `/docs/api-reference/files`
- List workspace files -> `/docs/api-reference/files/get-api-v1-files`
- Read file content -> `/docs/api-reference/files/get-api-v1-files-fileid-content`
- Create file -> `/docs/api-reference/files/post-api-v1-files`
- Complete file upload -> `/docs/api-reference/files/post-api-v1-files-fileid-complete`
- Presign multipart parts -> `/docs/api-reference/files/post-api-v1-files-fileid-parts`
- Update file content -> `/docs/api-reference/files/put-api-v1-files-fileid-content`
- Messages -> `/docs/api-reference/messages`
- List channel messages -> `/docs/api-reference/messages/get-api-v1-channels-channelid-messages`
- List thread messages -> `/docs/api-reference/messages/get-api-v1-channels-channelid-threads-threadid-messages`
- Create channel message -> `/docs/api-reference/messages/post-api-v1-channels-channelid-messages`
- Create thread message -> `/docs/api-reference/messages/post-api-v1-channels-channelid-threads-threadid-messages`
- Notifications -> `/docs/api-reference/notifications`
- List notification devices -> `/docs/api-reference/notifications/get-api-v1-notifications-devices`
- Check Expo notification receipts -> `/docs/api-reference/notifications/post-api-v1-notifications-receipts`
- Send workspace notifications -> `/docs/api-reference/notifications/post-api-v1-notifications-send`
- Runs -> `/docs/api-reference/runs`
- List workspace runs -> `/docs/api-reference/runs/get-api-v1-runs`
- Get workspace run -> `/docs/api-reference/runs/get-api-v1-runs-runid`
- Threads -> `/docs/api-reference/threads`
- List channel threads -> `/docs/api-reference/threads/get-api-v1-channels-channelid-threads`
- Get thread -> `/docs/api-reference/threads/get-api-v1-channels-channelid-threads-threadid`
- Create thread -> `/docs/api-reference/threads/post-api-v1-channels-channelid-threads`
- Workflows -> `/docs/api-reference/workflows`
- Delete workflow definition -> `/docs/api-reference/workflows/delete-api-v1-workflows-id`
- List workflow definitions -> `/docs/api-reference/workflows/get-api-v1-workflows`
- Get workflow definition -> `/docs/api-reference/workflows/get-api-v1-workflows-id`
- Update workflow definition -> `/docs/api-reference/workflows/patch-api-v1-workflows-id`
- Create workflow definition -> `/docs/api-reference/workflows/post-api-v1-workflows`
- Workspaces -> `/docs/api-reference/workspaces`
- Delete workspace API key -> `/docs/api-reference/workspaces/delete-api-v1-workspaces-workspacehandle-api-keys-keyid`
- Delete workspace credential -> `/docs/api-reference/workspaces/delete-api-v1-workspaces-workspacehandle-credentials-credentialid`
- Delete workspace invitation -> `/docs/api-reference/workspaces/delete-api-v1-workspaces-workspacehandle-invitations-invitationid`
- Delete workspace user -> `/docs/api-reference/workspaces/delete-api-v1-workspaces-workspacehandle-users-userid`
- List my workspaces -> `/docs/api-reference/workspaces/get-api-v1-workspaces`
- List workspace API keys -> `/docs/api-reference/workspaces/get-api-v1-workspaces-workspacehandle-api-keys`
- List workspace credentials -> `/docs/api-reference/workspaces/get-api-v1-workspaces-workspacehandle-credentials`
- List workspace credential tools -> `/docs/api-reference/workspaces/get-api-v1-workspaces-workspacehandle-credentials-credentialid-tools`
- List workspace invitations -> `/docs/api-reference/workspaces/get-api-v1-workspaces-workspacehandle-invitations`
- List workspace users -> `/docs/api-reference/workspaces/get-api-v1-workspaces-workspacehandle-users`
- Create workspace -> `/docs/api-reference/workspaces/post-api-v1-workspaces`
- Create workspace API key -> `/docs/api-reference/workspaces/post-api-v1-workspaces-workspacehandle-api-keys`
- Create workspace credential -> `/docs/api-reference/workspaces/post-api-v1-workspaces-workspacehandle-credentials`
- Invite user to workspace -> `/docs/api-reference/workspaces/post-api-v1-workspaces-workspacehandle-invitations`
- Create workspace user -> `/docs/api-reference/workspaces/post-api-v1-workspaces-workspacehandle-users`
- Apps -> `/docs/apps` (source: `content/docs/apps.mdx`)
- Authentication -> `/docs/authentication` (source: `content/docs/authentication.mdx`)
- Building Blocks -> `/docs/building-blocks` (source: `content/docs/building-blocks.mdx`)
- Data Model -> `/docs/data-model` (source: `content/docs/data-model.mdx`)
- Embedded Host Actions -> `/docs/embedded-host-actions` (source: `content/docs/embedded-host-actions.mdx`)
- Introduction -> `/docs/introduction` (source: `content/docs/introduction.mdx`)
- Recipes -> `/docs/receipes` (source: `content/docs/receipes/index.mdx`)
- Construction Site Visit Agent -> `/docs/receipes/construction-site-visit-agent` (source: `content/docs/receipes/construction-site-visit-agent.mdx`)
- Daily Lead Qualification Agent -> `/docs/receipes/daily-lead-qualification-agent` (source: `content/docs/receipes/daily-lead-qualification-agent.mdx`)
- Day Planner Agent -> `/docs/receipes/day-planner-agent` (source: `content/docs/receipes/day-planner-agent.mdx`)
- Legal Case Tracker -> `/docs/receipes/legal-case-tracker` (source: `content/docs/receipes/legal-case-tracker.mdx`)
- Spare Parts Request Agent -> `/docs/receipes/spare-parts-request-agent` (source: `content/docs/receipes/spare-parts-request-agent.mdx`)
- Weekly Report Email Agent -> `/docs/receipes/weekly-report-email-agent` (source: `content/docs/receipes/weekly-report-email-agent.mdx`)
- UI Components -> `/docs/ui-components` (source: `content/docs/ui-components.mdx`)