> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brussle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SDKs

> Python and TypeScript clients, generated from the API contract.

Both SDKs are thin. They are generated from the same OpenAPI document as the API reference, with no caching and no validation beyond types. They add a few things of their own: they [retry](#errors-and-retries) failed requests that are safe to repeat, [import](#importing-documents) large sets of documents in batches, page through the [events feed](#the-events-feed), and [verify webhook signatures](#verifying-webhooks). Any request the SDK makes, you can make with `curl` against `https://api.brussle.com/v1`.

| | Python | TypeScript |
| - | - | - |
| Install | `pip install brussle` | `npm install brussle` |
| Runtime | Python 3.9+ | Node 20.19+, or any runtime with `fetch` |
| Version | 1.x, for API `/v1` | 1.x, for API `/v1` |

## Methods

Methods mirror the routes. TypeScript uses the same names in camelCase.

```python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
db = Client(api_key=...)
ns = db.namespace("acme/prod/tenant_123")

ns.judgments.create(name=..., type=..., question=..., context=..., engine=..., thresholds=..., activate=True)
ns.judgments.create(from_starter="ticket_triage.urgency", paths={"subject": "state.subject", "body": "state.body"}, engine=..., dry_run=True)
ns.write(upsert=[...], patch=[...], append=[...], delete=[...], wait_for=[...])
ns.query(filters=..., rank_by=..., top_k=..., include=..., answers="fresh_only")
ns.get("t_123", include_history=True)
ns.judgments.update("needs_escalation", freshness={"policy": "on_change"}, confirm=True)
ns.judgments.activate("needs_escalation", version=4)
ns.judgments.backfill("needs_escalation", confirm=False)
ns.outcomes.append([...])
db.jobs.get(job_id); db.jobs.pause(job_id); db.jobs.resume(job_id); db.jobs.cancel(job_id); db.jobs.confirm(job_id)
ns.import_documents(records, concurrency=4)
```

The rest of the API:

| Route | Python | TypeScript |
| - | - | - |
| `GET /namespaces` | `db.namespaces.list(prefix=...)` | `db.namespaces.list({ prefix })` |
| `GET /namespaces/{ns}` | `ns.metadata()` | `ns.metadata()` |
| `PATCH /namespaces/{ns}` | `ns.update(budget=...)` | `ns.update({ budget })` |
| `POST /namespaces/{ns}/warm` | `ns.warm()` | `ns.warm()` |
| `DELETE /namespaces/{ns}` | `ns.delete()` | `ns.delete()` |
| `GET /namespaces/{ns}/judgments` | `ns.judgments.list()` | `ns.judgments.list()` |
| `GET /namespaces/{ns}/judgments/{name}` | `ns.judgments.get(name)` | `ns.judgments.get(name)` |
| `DELETE /namespaces/{ns}/judgments/{name}` | `ns.judgments.delete(name)` | `ns.judgments.delete(name)` |
| `PATCH /namespaces/{ns}/judgments/{name}` with thresholds | `ns.judgments.update(name, thresholds=...)` | `ns.judgments.update(name, { thresholds })` |
| `GET /namespaces/{ns}/judgments/{name}/calibration` | `ns.judgments.calibration(name)` | `ns.judgments.calibration(name)` |
| `GET /namespaces/{ns}/judgments/{name}/thresholds/recommend` | `ns.judgments.recommend_threshold(name, target="precision:0.9")` | `ns.judgments.recommendThreshold(name, { target: "precision:0.9" })` |
| `GET /namespaces/{ns}/judgments/{name}/labelling-queue` | `ns.judgments.labelling_queue(name)` | `ns.judgments.labellingQueue(name)` |
| `POST /namespaces/{ns}/judgments/{name}/labelling-queue` | `ns.judgments.draw_labelling_queue(name, count=50)` | `ns.judgments.drawLabellingQueue(name, { count: 50 })` |
| `GET /namespaces/{ns}/judgments/{name}/recipe-suggestion` | `ns.judgments.recipe_suggestion(name)` | `ns.judgments.recipeSuggestion(name)` |
| `POST /namespaces/{ns}/judgments/{name}/recipe-tuning` | `ns.judgments.tune_recipe(name)` | `ns.judgments.tuneRecipe(name)` |
| `POST /namespaces/{ns}/judgments/{name}/detach` | `ns.judgments.detach(name)` | `ns.judgments.detach(name)` |
| `POST /namespaces/{ns}/judgments/{name}/suggest_parts` | `ns.judgments.suggest_parts(name, count=5)` | `ns.judgments.suggestParts(name, { count: 5 })` |
| `POST /namespaces/{ns}/judgments/{name}/discover` | `ns.judgments.discover(name, window="7d", count=5)` | `ns.judgments.discover(name, { window: "7d", count: 5 })` |
| `POST /namespaces/{ns}/judgments/{name}/simulate` | `ns.judgments.simulate(name, relation=..., document=...)` | `ns.judgments.simulate(name, { relation, document })` |
| `POST /namespaces/{ns}/groups` | `ns.groups.create(name=..., key=..., aggregate=...)` | `ns.groups.create({ name, key, aggregate })` |
| `GET /namespaces/{ns}/groups` | `ns.groups.list()` | `ns.groups.list()` |
| `GET /namespaces/{ns}/groups/{name}` | `ns.groups.get(name)` | `ns.groups.get(name)` |
| `PATCH /namespaces/{ns}/groups/{name}` | `ns.groups.update(name, aggregate=...)` | `ns.groups.update(name, { aggregate })` |
| `DELETE /namespaces/{ns}/groups/{name}` | `ns.groups.delete(name)` | `ns.groups.delete(name)` |
| `POST /namespaces/{ns}/tenant_summary` | `ns.tenant_summary.create(groups=..., bands=...)` | `ns.tenantSummary.create({ groups, bands })` |
| `GET /namespaces/{ns}/tenant_summary` | `ns.tenant_summary.get()` | `ns.tenantSummary.get()` |
| `PATCH /namespaces/{ns}/tenant_summary` | `ns.tenant_summary.update(bands=..., confirm=True)` | `ns.tenantSummary.update({ bands, confirm: true })` |
| `DELETE /namespaces/{ns}/tenant_summary` | `ns.tenant_summary.delete()` | `ns.tenantSummary.delete()` |
| `GET /namespaces/{ns}/evaluations/{id}` | `ns.evaluation(id)` | `ns.evaluation(id)` |
| `POST /namespaces/{ns}/evaluations/exports` ([Scale](/guides/export-history)) | `ns.export_evaluations(since=..., until=...)`, then `db.jobs.get(job_id)` | `ns.exportEvaluations({ since, until })`, then `db.jobs.get(jobId)` |
| `GET /templates` | `db.templates.list(prefix=...)` | `db.templates.list({ prefix })` |
| `GET /engines` | `db.engines.list()` | `db.engines.list()` |
| `GET /starters` | `db.starters.list()` | `db.starters.list()` |
| `GET /nonproduction-prefixes` | `db.nonproduction_prefixes.list()` | `db.nonproductionPrefixes.list()` |
| `PUT /nonproduction-prefixes/{prefix}` | `db.nonproduction_prefixes.mark(prefix)` | `db.nonproductionPrefixes.mark(prefix)` |
| `DELETE /nonproduction-prefixes/{prefix}` | `db.nonproduction_prefixes.unmark(prefix)` | `db.nonproductionPrefixes.unmark(prefix)` |
| `POST /namespaces/{ns}/subscriptions` | `ns.subscriptions.create(name=..., filters=..., endpoint=...)` | `ns.subscriptions.create({ name, filters, endpoint })` |
| `GET /namespaces/{ns}/subscriptions` | `ns.subscriptions.list()` | `ns.subscriptions.list()` |
| `GET /namespaces/{ns}/subscriptions/{name}` | `ns.subscriptions.get(name)` | `ns.subscriptions.get(name)` |
| `PATCH /namespaces/{ns}/subscriptions/{name}` | `ns.subscriptions.update(name, events=...)` | `ns.subscriptions.update(name, { events })` |
| `DELETE /namespaces/{ns}/subscriptions/{name}` | `ns.subscriptions.delete(name)` | `ns.subscriptions.delete(name)` |
| `POST /namespaces/{ns}/subscriptions/{name}/query` | `ns.subscriptions.query(name, top_k=100)` | `ns.subscriptions.query(name, { top_k: 100 })` |
| `POST /webhook-endpoints` | `db.webhook_endpoints.create(url=..., events=[...])` | `db.webhookEndpoints.create({ url, events })` |
| `GET /webhook-endpoints` | `db.webhook_endpoints.list()` | `db.webhookEndpoints.list()` |
| `GET /webhook-endpoints/{id}` | `db.webhook_endpoints.get(id)` | `db.webhookEndpoints.get(id)` |
| `PATCH /webhook-endpoints/{id}` | `db.webhook_endpoints.update(id, enabled=True)` | `db.webhookEndpoints.update(id, { enabled: true })` |
| `DELETE /webhook-endpoints/{id}` | `db.webhook_endpoints.delete(id)` | `db.webhookEndpoints.delete(id)` |
| `POST /webhook-endpoints/{id}/rotate-secret` | `db.webhook_endpoints.rotate_secret(id, previous_valid_for="24h")` | `db.webhookEndpoints.rotateSecret(id, { previous_valid_for: "24h" })` |
| `POST /webhook-endpoints/{id}/test` | `db.webhook_endpoints.test(id, type="job.completed")` | `db.webhookEndpoints.test(id, { type: "job.completed" })` |
| `GET /webhook-endpoints/{id}/deliveries` | `db.webhook_endpoints.deliveries(id, status="failed", cursor=...)` | `db.webhookEndpoints.deliveries(id, { status: "failed", cursor })` |
| `POST /webhook-endpoints/{id}/recover` | `db.webhook_endpoints.recover(id, since=...)` | `db.webhookEndpoints.recover(id, { since })` |
| `GET /events` | `db.events.list(cursor=..., types=[...])`, or `db.events.iterate(...)` | `db.events.list({ cursor, types })`, or `db.events.iterate(...)` |
| `GET /events/{id}` | `db.events.get(id)` | `db.events.get(id)` |
| `POST /events/{id}/redeliver` | `db.events.redeliver(id, endpoint=...)` | `db.events.redeliver(id, { endpoint })` |

Namespace names and document ids may contain `/`. The SDKs send it as `%2F`, as the API requires. An id or name that is empty, `.` or `..` is refused before any request is sent (a `ValueError` in Python, a `TypeError` in TypeScript), because a URL would read it as a different path. A [template](/guides/templates) is a namespace handle on a prefix ending in `/*`, such as `db.namespace("acme/prod/*")`, and its `*` is sent as it is.

## Requests and responses are the API's JSON

Request and response bodies use the API's own field names (`wait_for`, `top_k`, `rank_by`) in both languages. Any JSON body in these docs, or one copied from the dashboard's query builder, can be passed to the SDK unchanged. Options that are not part of a JSON body follow each language's conventions, such as `include_history=True` in Python and `{ includeHistory: true }` in TypeScript.

The types are strict:

* **Answers and definitions** are discriminated unions on `type`. In TypeScript, `answer.p` type-checks only after `answer.type === "bool"`.
* **Filters** are typed tuples: `[field, op, value]`, `["And" | "Or", [...]]` or `["Not", filter]`. An unknown operator is a type error.

In Python, filters are tuples. mypy cannot check a nested tuple literal, so use `And`, `Or` and `Not`:

```python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
from brussle import And, Not

ns.query(filters=And(
    ("attributes.plan", "Eq", "pro"),
    Not(("answers.needs_escalation.freshness", "Eq", "stale")),
))
```

Python responses are plain dicts, typed with `TypedDict`s in `brussle.types`. Nothing is validated at runtime, so fields and enum values the API adds later never break your client.

## Errors and retries

Every error response becomes an `ApiError` with the HTTP `status`, the API's `code` (such as `budget_exceeded` or `too_large`), its `message` and `details`. An error response that is not the API's JSON, such as a proxy's error page, or a success whose body is not JSON, gets the code `unknown`, a generic message and, when the response has one, `details.request_id`. Network errors, and timeouts in Python, are raised as they are.

Each call is retried at most twice by default. Set `max_retries` in Python or `maxRetries` in TypeScript on the client to change it.

* **`429` or `503` with `Retry-After`.** Retried after waiting that long, for any call: the API refused it without acting on it, or it was a write, which is always safe to repeat. A `Retry-After` over 60 seconds (`MAX_RETRY_AFTER_SECONDS`) is not waited out, and the error is returned at once. A daily allowance that is used up sends no `Retry-After` and says when it renews in `details.resets_at`.
* **Network errors, timeouts, `500`, `502`, `503` and `504`.** Retried only when the call is safe to repeat: a read (every `GET`, `query`, and a subscription's `query`), a `write`, which is idempotent by construction, a call that changes nothing when repeated (marking or unmarking a non-production prefix, updating a subscription or webhook endpoint, recovering an endpoint's deliveries), or any call with an idempotency key. Creating an endpoint or subscription, rotating a secret, a test send and a redelivery are not retried without a key. The wait starts at 0.5 seconds and doubles on each retry up to 8 seconds, with random jitter, unless the response gives a `Retry-After`.
* **Anything else** is returned at once, including every other `4xx`, and a failed call that is not safe to repeat, such as creating a judgment without a key.

### Idempotency keys

Every call that changes something takes an idempotency key, sent as the `Idempotency-Key` header: `idempotency_key="..."` in Python and `{ idempotencyKey: "..." }` as the last argument in TypeScript. Use one key per logical request, and reuse it only for that request's retries. A call with a key is retried like a write.

* **A retry gets the first response.** For 24 hours after a call succeeds, a call with the same key and the same arguments gets that response back, verbatim, instead of running again: a retried create returns the version it made, a retried backfill confirm the job it started, a retried draw the documents it leased, and a retried rotation the secret it made. The response carries the header `Idempotent-Replayed: true`.
* **A failed call runs again.** Only a successful response is kept, so retrying a call that failed runs it again.
* **One key, one request.** A key belongs to one route and one set of arguments in your organization. The same key with different arguments is refused with `idempotency_key_reused` (`422`) while the first call's response is kept.
* **A retry while the first call runs** is refused with `rate_limited` and `Retry-After: 1`, without running. The SDKs wait and retry, and get the first call's response once it finishes.
* **Keys** are 1 to 255 visible ASCII characters; an empty or longer one is `invalid_request`.

<CodeGroup>
  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  ns.judgments.create(
      name="needs_escalation",
      type="bool",
      question="Does this ticket need escalation?",
      context={"fields": ["state.subject", "state.body"]},
      engine={"name": "jev", "version": "current"},
      idempotency_key="create-needs-escalation-v1",
  )
  ```

  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  await ns.judgments.create(
    {
      name: "needs_escalation",
      type: "bool",
      question: "Does this ticket need escalation?",
      context: { fields: ["state.subject", "state.body"] },
      engine: { name: "jev", version: "current" },
    },
    { idempotencyKey: "create-needs-escalation-v1" },
  );
  ```
</CodeGroup>

Writes need no key to be retried safely: a retried append adds nothing while its values are still in the array, and a retried upsert or patch sets the same content again (see [system behavior](/behavior#your-data)). A key only saves the second write when the first one landed.

## Importing documents

`ns.import_documents(documents)` in Python and `ns.importDocuments(documents)` in TypeScript upsert a large set of documents. They read `documents` as they go, from any iterable (and in TypeScript any async iterable), so an import never has to fit in memory.

* **Batches.** Each write holds up to 1,000 documents or 64 MB, the most the API takes.
* **Concurrency.** Up to `concurrency` writes at once, 4 by default. Python runs them on a thread pool.
* **Retries.** Each batch has its own idempotency key, reused on its retries, which follow the client's rules above.
* **Repeated ids.** No id is in two writes at once. A document whose id is still being written waits for that write, so when your input repeats an id, the later document is the one that stays.
* **Failures.** A batch that still fails after its retries is recorded, and the import carries on. `stop_on_failure=True` (`stopOnFailure: true`) stops starting new batches after the first failure.
* **Progress.** `on_progress` (`onProgress`) is called after each batch with the totals so far.

It returns a summary: `documents` and `batches` written, `failures`, each with the batch's `ids` and its `error`, and `stopped`. A write commits all of its documents or none, and writing them again is safe, so once you have fixed the cause, write a failed batch's ids again. See [import existing data](/guides/import-existing-data).

## Webhooks and events

A subscription is a saved query: it sends `subscription.entered` when a document starts matching, and `subscription.exited` when it stops if you ask for it. Platform events such as `job.completed` go to the endpoints whose `events` patterns match them. Every event also goes to the events feed. See [webhooks](/guides/webhooks) for how deliveries work.

<CodeGroup>
  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  from brussle import And

  endpoint = db.webhook_endpoints.create(url="https://example.com/hooks/events", events=["job.*"])
  secret = endpoint["secret"]  # keep it where your receiver can read it

  ns = db.namespace("acme/prod/*")  # a template prefix covers every tenant under it
  ns.subscriptions.create(
      name="urgent-enterprise",
      filters=And(("answers.urgency.p", "Gt", 0.8), ("attributes.plan", "Eq", "enterprise")),
      events=["entered", "exited"],
      endpoint=endpoint["id"],
  )
  db.webhook_endpoints.test(endpoint["id"])  # sends webhook.test, marked "test": true
  ```

  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  const endpoint = await db.webhookEndpoints.create({ url: "https://example.com/hooks/events", events: ["job.*"] });
  const secret = endpoint.secret; // keep it where your receiver can read it

  const ns = db.namespace("acme/prod/*"); // a template prefix covers every tenant under it
  await ns.subscriptions.create({
    name: "urgent-enterprise",
    filters: ["And", [["answers.urgency.p", "Gt", 0.8], ["attributes.plan", "Eq", "enterprise"]]],
    events: ["entered", "exited"],
    endpoint: endpoint.id,
  });
  await db.webhookEndpoints.test(endpoint.id); // sends webhook.test, marked "test": true
  ```
</CodeGroup>

`rotate_secret` / `rotateSecret` returns a new secret. The old one keeps signing beside it for `previous_valid_for`, 24 hours by default, so your receivers can switch over without missing an event.

### The events feed

`db.events.list(...)` returns one page of the feed, oldest first, with a `next_cursor` that is present even on an empty page. `db.events.iterate(...)` follows `next_cursor` for you and yields each event until a page comes back empty. Both take `cursor`, `limit`, `types` (patterns such as `job.*`) and `namespace_prefix` (`namespacePrefix`). An event's `id` also works as a cursor, so a poller can store the last id it handled and resume after it (see [the events feed](/guides/events-feed)):

<CodeGroup>
  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  for event in db.events.iterate(cursor=last_seen, types=["subscription.*"], namespace_prefix="acme/prod/"):
      handle(event)
      last_seen = event["id"]
  ```

  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  for await (const event of db.events.iterate({ cursor: lastSeen, types: ["subscription.*"], namespacePrefix: "acme/prod/" })) {
    await handle(event);
    lastSeen = event.id;
  }
  ```
</CodeGroup>

The feed keeps 30 days. An older cursor fails with `invalid_request` and `details.reason` `cursor_expired`: re-read the current state, for a subscription with its `query`, and start again without a cursor.

### Verifying webhooks

`verify_webhook(payload, headers, secret)` in Python and `verifyWebhook(payload, headers, secret)` in TypeScript check a delivery's signature (Standard Webhooks, HMAC-SHA256) and its `webhook-timestamp`, which must be within 5 minutes of your clock, and return the parsed event. They need nothing beyond the SDK in Python and WebCrypto in TypeScript, so they run on edge runtimes too. `secret` may also be a list of secrets. A request that fails a check raises `WebhookVerificationError`, whose `reason` is `headers`, `timestamp` or `signature`: answer it with a `400`. An empty or malformed secret, or an empty list, raises an ordinary error instead (`ValueError` in Python, `TypeError` in TypeScript), before the request is checked: it never verifies anything.

<Warning>
  Pass the raw body, exactly the bytes you received. A body parsed as JSON and serialized again can differ by a space or a key order, and its signature will not match.
</Warning>

<CodeGroup>
  ```python Flask theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  import os

  from flask import Flask, request
  from brussle import WebhookVerificationError, verify_webhook

  WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]  # fails at start-up when unset
  app = Flask(__name__)

  @app.post("/hooks/events")
  def events():
      try:
          event = verify_webhook(request.get_data(), request.headers, WEBHOOK_SECRET)
      except WebhookVerificationError:
          return "", 400
      handle(event)
      return "", 204
  ```

  ```python FastAPI theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  import os

  from fastapi import FastAPI, Request, Response
  from brussle import WebhookVerificationError, verify_webhook

  WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]  # fails at start-up when unset
  app = FastAPI()

  @app.post("/hooks/events")
  async def events(request: Request) -> Response:
      try:
          event = verify_webhook(await request.body(), request.headers, WEBHOOK_SECRET)
      except WebhookVerificationError:
          return Response(status_code=400)
      handle(event)
      return Response(status_code=204)
  ```

  ```ts Next.js theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  // app/hooks/events/route.ts
  import { verifyWebhook, WebhookVerificationError } from "brussle";

  const secret = process.env.WEBHOOK_SECRET;
  if (!secret) throw new Error("WEBHOOK_SECRET is not set");

  // An arrow function, so TypeScript keeps `secret` narrowed to a string inside it.
  export const POST = async (request: Request) => {
    const body = new Uint8Array(await request.arrayBuffer());
    try {
      const event = await verifyWebhook(body, request.headers, secret);
      await handle(event);
    } catch (error) {
      if (error instanceof WebhookVerificationError) return new Response(null, { status: 400 });
      throw error;
    }
    return new Response(null, { status: 204 });
  };
  ```

  ```ts Express theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  import express from "express";
  import { verifyWebhook, WebhookVerificationError } from "brussle";

  const app = express();
  const secret = process.env.WEBHOOK_SECRET;
  if (!secret) throw new Error("WEBHOOK_SECRET is not set");

  // express.raw keeps the body as the bytes received.
  app.post("/hooks/events", express.raw({ type: "application/json" }), async (req, res) => {
    try {
      const event = await verifyWebhook(req.body, req.headers, secret);
      await handle(event);
      res.sendStatus(204);
    } catch (error) {
      if (error instanceof WebhookVerificationError) res.sendStatus(400);
      else throw error;
    }
  });
  ```
</CodeGroup>

The event is typed as `WebhookEvent`, a union discriminated on `type`, so checking `type` narrows `data` to that event's fields. `test` is `true` on every event a test send sends. New event types may be added, so ignore the ones you don't handle:

<CodeGroup>
  ```python Python theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  from brussle.types import WebhookEvent

  def handle(event: WebhookEvent) -> None:
      if event.get("test"):
          return
      if event["type"] == "subscription.entered":
          escalate(event["data"]["namespace"], event["data"]["document"]["id"])
      elif event["type"] == "job.completed":
          print("job done:", event["data"]["job"]["id"])
  ```

  ```ts TypeScript theme={"theme":{"light":"css-variables","dark":"css-variables"}}
  import type { WebhookEvent } from "brussle";

  async function handle(event: WebhookEvent) {
    if (event.test) return;
    switch (event.type) {
      case "subscription.entered":
        await escalate(event.data.namespace, event.data.document.id);
        break;
      case "job.completed":
        console.log("job done:", event.data.job.id);
        break;
    }
  }
  ```
</CodeGroup>

Deliveries are at least once and may arrive out of order: dedupe on the event's `id` (the `webhook-id` header), and for one subscription and document, drop an event whose `data.sequence` is lower than one you already handled.


## Related topics

- [Receive webhooks](/guides/webhooks.md)
- [Read the events feed](/guides/events-feed.md)
- [Templates](/guides/templates.md)
- [Namespaces](/concepts/namespaces.md)
- [Import existing data](/guides/import-existing-data.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.