# Plans, invoices, and usage
Source: https://docs.tale.dev/cloud/billing
Your Cloud service agreement determines what you pay for hosting, support, seats, and storage. Model usage is a separate consideration: the provider and model you use affect the cost of each AI request.
## Understand the charges
Tale offers the free, self-hosted Community edition and Enterprise for managed Cloud or supported self-hosted deployments. Both include the same product features. Enterprise adds professional services and support; it does not unlock a separate set of product controls.
The [pricing page](https://tale.dev/pricing) is the source for current seat and storage rates, billing periods, and included services. It lists AI usage at provider rates without a markup. Use your agreed quote and service agreement for the terms that apply to your organization.
## Find an invoice or change billing details
Contact the Tale team through your Enterprise support channel for invoices, billing details, seat changes, or questions about a charge. There is no **Settings > Billing** page in the shared product interface. Usage dashboards are operational records, not an invoice portal.
When querying a charge, include the billing period, organization, and invoice reference. Never include provider keys or API keys.
## Understand model usage
Open [Usage analytics](https://docs.tale.dev/platform/admin/governance/usage-analytics) to see the usage recorded by the platform. Use the date range and model or user breakdowns to find what generated the activity.
A displayed usage cost and a final invoice serve different purposes. Your commercial agreement and the provider’s billing rules determine the amount payable; do not treat a dashboard total as a final invoice or tax statement.
Before rolling out a new model, try a representative task and review its quality and recorded usage. A cheaper request is useful only if it produces a result your team can use.
## Set limits for the workspace
Use [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits) to configure the controls your team needs. Check the scope of each rule and test it with an account in that scope. Platform limits apply to the activity they govern; they do not change the terms of your hosting contract.
For self-hosted Community, you operate the infrastructure and pay your chosen providers directly. The same usage and policy pages help you understand that activity.
# Understand Cloud data residency
Source: https://docs.tale.dev/cloud/data-residency
Data residency covers where your data is stored and where it is processed. Choosing a Cloud region addresses the hosted service; it does not by itself determine where every model provider or connected service handles your data.
## Confirm the hosting arrangement
Confirm your instance’s primary region, backup locations, retention, recovery objectives, and support process with Tale before onboarding. Use the service agreement and data-processing documents for the commitments that apply to your deployment. Do not infer a backup city or recovery guarantee from a region label in the product.
The Cloud service is operated by Tale. Configuration files, database credentials, and host environment variables are operator responsibilities. The [self-hosted configuration reference](https://docs.tale.dev/self-hosted/configuration/data-residency) explains the technical model for operators.
## Follow the data through a request
A chat message travels to your Tale instance. When the assistant uses knowledge, relevant content is retrieved from the organization’s knowledge store. The message and selected context are then sent to the model provider used for that response. A tool may contact another service, such as a website or a connected application.
| Data flow | What to confirm |
| --- | --- |
| Stored chats, documents, and configuration | The agreed hosting and backup locations |
| Knowledge indexing | Which embedding provider receives document content |
| Model inference | The selected provider’s endpoint, processing terms, and retention |
| Connectors and web tools | Which external systems receive requests and content |
| Operational records | The agreed handling of logs, backups, and support access |
A provider may offer regional or locally hosted endpoints. Verify the endpoint actually configured; its brand name alone does not establish where processing occurs.
Review the embedding provider as well as the chat model. A document can be sent for embedding during indexing before anyone asks a question about it.
## Review a new integration
Before connecting a service, identify what data the intended task will send and which account the connector uses. Check the service’s processing terms, restrict its access, and test with non-sensitive example content. Record the decision alongside your [security review](https://docs.tale.dev/cloud/trust-and-compliance).
## Change the region
Arrange a region change with Tale. It requires a migration plan covering stored data, backups, external endpoints, downtime, and validation. Creating a second organization does not move the first organization’s data.
The [migration planning guide](https://docs.tale.dev/cloud/migrate-to-self-hosted) lists the questions and verification steps that also apply when moving between Cloud regions.
# Tale Cloud
Source: https://docs.tale.dev/cloud
Tale Cloud is the managed way to run Tale. Your team uses the same product features as the open-source edition, while Tale operates the service under your Enterprise agreement.
## Start with your next task
Request an instance or join the one your organization already uses.
Separate service charges from model usage and find the right support channel.
Check hosting arrangements and the providers and tools that process content.
Gather certification evidence and review your responsibilities.
Coordinate an operator-assisted migration and verify the destination.
For chats, projects, agents, and settings, use the [Platform guides](https://docs.tale.dev/platform). Cloud pages cover the hosting service and its responsibilities; the product workflows are shared.
# Plan a move to self-hosted Tale
Source: https://docs.tale.dev/cloud/migrate-to-self-hosted
Moving from Cloud to self-hosted Tale transfers responsibility for the infrastructure to your team. Plan the move with Tale and the operator of the destination so application data, knowledge, files, configuration, and encryption keys remain consistent.
Plan an operator-assisted transfer of the databases, files, configuration and required secrets. Individual API exports cover selected resources; they do not replace a consistent backup of the instance.
## Decide what the move must preserve
List the organizations and data to move, the permitted downtime, the target version, and the people who will approve the result. Confirm the infrastructure requirements in the [self-hosted installation guide](https://docs.tale.dev/self-hosted/install/quickstart).
| Area | Questions to resolve before the move |
| --- | --- |
| Application database | Which backup is the consistent source, and which versions can restore it? |
| Knowledge stores | Which per-organization stores, indexes, and embedding settings must move? |
| Files and configuration | Which object-store data and configuration directories belong to the deployment? |
| Encryption | Which encryption and signing keys must be retained securely? |
| External services | Which callback URLs, webhook destinations, credentials, or network rules change? |
| Background work | Which runs must finish or be paused before the final copy? |
Use the [backup and restore guide](https://docs.tale.dev/self-hosted/operate/backups-and-restore) with your operator. Do not substitute a collection of API exports for that plan.
## Rehearse on an isolated destination
Restore a copy into an isolated environment before the cutover. Keep outbound automations and scheduled jobs controlled so the rehearsal cannot send duplicate messages or make unintended external changes.
Verify sign-in, roles, representative documents, project files, a chat response, and the configuration of critical integrations. Compare counts and selected records with the source. A service that boots has passed only the first check.
## Agree on the cutover and recovery
Write down who freezes writes, takes the final copy, changes routing, and validates the new instance. Agree on what triggers a rollback and how to prevent both instances accepting changes at once. Keep the source and verified backups available until the migration is accepted.
Update public origins, TLS, SSO callbacks, and integration destinations as required by the new address. Existing sessions or external credentials may need renewal; test them rather than assuming they transfer.
## Hand over operation
Confirm monitoring, backup schedules, restore ownership, upgrade procedures, and support contacts. Record the accepted checks and tell the team which address to use. Your next ongoing responsibilities are [upgrades](https://docs.tale.dev/self-hosted/operate/upgrades), [observability](https://docs.tale.dev/self-hosted/operate/observability/operations), and rehearsed restores.
# Get started on Tale Cloud
Source: https://docs.tale.dev/cloud/onboarding
Cloud onboarding starts with the instance your organization will use. Once you have access, the same chat, project, and administration guides apply as on a self-hosted instance.
## Join an existing instance
Ask your administrator for the instance address and sign-in method. Open that address and use your own account. If the organization uses SSO, follow its sign-in flow rather than creating a second account.
After signing in, follow [send your first message](https://docs.tale.dev/get-started/quickstart). If you cannot see the expected organization or project, ask the administrator to check membership, role, and project sharing.
## Set up a new Cloud service
[Request a demo](https://tale.dev/request-demo) or contact the Tale team. Agree on the hosting region, service terms, identity requirements, and the person who will administer the workspace. Use [data residency](https://docs.tale.dev/cloud/data-residency) and [security and compliance](https://docs.tale.dev/cloud/trust-and-compliance) to prepare those decisions.
Use the instance address and setup instructions supplied by Tale. If the first-run wizard is shown, create the initial account and organization. If an organization is already present, open it; creating another organization produces a separate workspace.
If organization creation is managed by the operator, the organization picker does not offer a creation action. A direct setup link explains the restriction. Contact the operator to request another workspace.

Follow [set up a workspace](https://docs.tale.dev/get-started/admins) to add provider credentials, verify a model response, and create member accounts with the appropriate roles. The workspace can exist before a provider is ready; a working chat is the check that model access is configured.
## Confirm the first useful workflow
Choose one task the team actually needs: discuss a document, organize project work, or test a project agent. Check the result with the intended user role. [Use Tale with your team](https://docs.tale.dev/get-started/members) and [create a project agent](https://docs.tale.dev/get-started/editors) provide the next steps.
Test with a small, non-sensitive sample before importing a large collection. This lets you check access and data flows while the setup is still small enough to understand.
## Get help with access or setup
For account and project access, start with your organization’s administrator. For instance availability, service configuration, or contract questions, use your agreed Tale support channel. Include the instance address, time, action, and displayed error; exclude passwords and keys.
# Security and compliance
Source: https://docs.tale.dev/cloud/trust-and-compliance
Ruler GmbH, the company behind Tale, is ISO/IEC 27001 certified. The certification covers its Tale Enterprise offering and services. For a security review, ask your Tale contact for the applicable certificate, its scope, and supporting documents for the service your organization has purchased.
Product controls support your organization’s processes. Whether a particular use meets your obligations also depends on your configuration, connected providers, and operating procedures.
## Prepare a review
Bring together your service agreement, data-processing agreement, the relevant certification evidence, and a description of your deployment. The [privacy policy](https://docs.tale.dev/legal/privacy) and [subprocessor information](https://docs.tale.dev/legal/subprocessors) provide additional context. Record the version and scope of each document in your review.
Clarify which organization and deployment the review covers. A certification statement is not a substitute for checking whether a particular service or configuration falls within the report’s scope.
## Know who is responsible
| Area | Tale on Cloud | Your organization |
| --- | --- | --- |
| Hosting and maintenance | Operates the agreed service | Chooses the service and coordinates changes |
| Identity and access | Provides account, role, and SSO controls | Adds members, grants access, and reviews it |
| Model providers and connectors | Provides integration controls | Chooses services, credentials, and permitted uses |
| Usage and content policies | Provides policy controls and records | Configures rules and responds to events |
| Data requests and retention | Provides the supported workflows | Determines requirements and authorizes actions |
For self-hosted deployments, your operator also owns the infrastructure responsibilities. Enterprise support arrangements depend on your agreement.
## Check the controls in the product
- [Members and roles](https://docs.tale.dev/platform/admin/members-and-roles) define access. Review inactive accounts and elevated roles.
- [Enterprise SSO](https://docs.tale.dev/platform/admin/enterprise-sso) connects your identity provider. Test both sign-in and recovery before requiring it.
- [Audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs) help investigate recorded actions. Use [audit-log integrity](https://docs.tale.dev/self-hosted/operate/security/audit-log-integrity) when evaluating tamper evidence and its limits.
- [Guardrails](https://docs.tale.dev/platform/admin/governance/guardrails), [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold), and [data-subject requests](https://docs.tale.dev/platform/admin/governance/data-subject-requests) support specific processes; read their scope before relying on them.
## Report an incident
Use your agreed Enterprise support channel for a service incident. Report a suspected vulnerability through [GitHub’s private security reporting](https://github.com/tale-project/tale/security) or `security@tale.dev`. Include the affected version and reproduction details without sending credentials or personal data in a public issue.
For a review of where data travels, continue with [Cloud data residency](https://docs.tale.dev/cloud/data-residency).
# Edit configuration with a coding agent
Source: https://docs.tale.dev/develop/ai-assisted-development
Use a coding agent to help edit a CLI-managed Tale configuration project. The project includes instructions, example configuration and a selected source reference. You still need to review the proposed change and verify which organizations it will affect.
This is different from contributing to Tale’s application source. For a source checkout, start with [Contributor setup](https://docs.tale.dev/develop/contributor-setup) and the repository’s own `AGENTS.md`.
## A worked setup
Install the [Tale CLI](https://docs.tale.dev/self-hosted/install/cli-install), then create a new directory for the configuration project:
```bash
tale init agent-config-example --no-env
cd agent-config-example
ls -a
```
The following is an excerpt of the generated paths:
```text
AGENTS.md
CLAUDE.md
default/
.gitignore
.tale/
tale.json
```
`--no-env` skips environment setup; it does not create a runnable deployment or generate `.env`. This is useful when you want to inspect configuration before starting containers. `tale dev` performs environment setup when you later launch locally. Review the [quickstart prerequisites](https://docs.tale.dev/self-hosted/install/quickstart) first.
Open this directory in your coding editor. Ask the agent to read `AGENTS.md`, the relevant existing configuration and the source under `.tale/reference/` before proposing a change.
## The two instruction files
`AGENTS.md` contains Tale’s configuration guidance. `CLAUDE.md` points to that file so there is one maintained set of project instructions. The CLI also recognizes an existing `AGENT.md`, or a `CLAUDE.md` already stored under `.claude/`.
The CLI owns the section between its `tale:begin` and `tale:end` comment markers. Put your project-specific conventions outside that section; initialization and updates preserve that surrounding content. Do not put credentials into either instruction file.
The CLI does not generate separate Cursor, Windsurf or Copilot rule files. If an editor does not automatically load the project instructions, explicitly include them in the agent’s context. Check the actual configuration schemas rather than relying on the agent’s memory of an earlier release.
## What lives where
| Path | How to use it |
| --- | --- |
| `default/agents/` | Agent configuration catalog, including the supplied coding-agent example. |
| `default/automations/` | Automation definitions available to install or deploy. Presence on disk does not mean an automation is active. |
| `default/skills/` | Skill bundles, including document and visual-analysis skills. Each bundle is a directory. |
| `default/branding/` | Branding configuration and image assets for the template. |
| `default/governance/` | Policy and retention examples. Follow the formats of the files generated by your CLI. |
| `default/README.md` | Explains the template and automatic-install behavior of its catalog. |
| `.tale/reference/` | Selected implementation source embedded in the CLI. Read it; changes here are replaced by regeneration. It is not a complete repository checkout. |
| `.tale/orgs///` | Runtime configuration of actual organizations created in the app. |
| `.tale/checksums.json` | Records scaffolded-file hashes so updates can distinguish your edits from generated content. |
`default/` is the template for new organizations, not a deployable organization itself. Editing it does not by itself update an existing organization. The `.tale/` directory and secret sidecars are ignored by Git; public template files are intended for version control.
## Keep the mirror fresh
`tale update` updates the CLI within its current release line and refreshes generated project content. It regenerates the reference, updates managed instruction sections, adds new catalog files and updates files whose checksums show no local changes. Locally changed catalog files are preserved unless you use `--force`.
Use `tale update --dry-run` to inspect the proposed changes. Keep a version-controlled copy of your public configuration and protected backups of runtime configuration and secrets. Do not treat `.tale/reference/` as a place to maintain a fork. Updating the CLI does not roll running containers; follow [Upgrades](https://docs.tale.dev/self-hosted/operate/upgrades) when changing the deployed version.
## Cursor: config plane vs runtime plane
An editor agent working in this directory changes local configuration files. A Tale [project agent](https://docs.tale.dev/platform/projects/project-agents) using the Cursor harness runs work in a Tale-managed sandbox. These are separate execution contexts with separate credentials and consequences.
The sandbox harness uses its configured provider account and model. Giving your editor the project instructions does not configure that account. Follow [Harnesses](https://docs.tale.dev/platform/agents/harnesses) for runtime setup.
## Review and apply a proposal
1. Ask for one bounded change and name whether it belongs to the new-organization template or an existing organization.
2. Check every changed path, schema field, slug and referenced credential. Keep secrets out of the prompt and public diff.
3. Validate or test through the corresponding product surface. For an automation, inspect validation results and run its mock tests before deployment.
4. Review the deployment plan and the destination organization. `tale deploy --override` can replace runtime configuration with the local copy; use it only for a deliberate, reviewed overwrite.
5. Read back the configuration and exercise the behavior after deployment.
If the agent proposes a field absent from the installed schema, stop at validation and correct the proposal. If an edit to `default/` leaves an existing organization unchanged, check the destination instead of repeatedly deploying the template.
# API reference
Source: https://docs.tale.dev/develop/api-reference
The REST API lets you read and change Tale resources with an API key: projects, files, tasks, automations, runs and chat threads. Start with [your first API request](https://docs.tale.dev/get-started/developers) to verify access, then use the operation-specific sections below.
Your instance serves the field-level OpenAPI schema at `/openapi.json` and an interactive reference at `/docs`. Use that instance’s schema when generating a client. This page explains permissions, scope, asynchronous work and errors that apply across those operations.
## A worked request
Set `TALE_URL` to your application origin, `TALE_API_KEY` to your key, and `TALE_ORG_SLUG` to the intended organization. Keep secrets in the environment. Begin with an identity check before creating resources:
```bash
curl --fail-with-body --compressed "$TALE_URL/api/v1/me" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG"
```
A `200` response identifies `user`, the selected `organization`, all current `organizations`, deployment `capabilities`, and this `key`'s name and expiration. Check the organization and role before continuing. The examples below use placeholder project, file, and run IDs: obtain actual IDs from a previous response rather than copying a display name or guessing an ID.
### Read every page of a list
List responses use named collections, not a bare array. The instance's OpenAPI `200` schemas declare `x-tale-pagination`; use it to choose the correct loop.
| Family | Requests | Response and stopping rule |
| --- | --- | --- |
| Keyset | `cursor`, `limit` | Rows plus `isDone` and `continueCursor`; stop at `isDone: true` |
| Offset: website pages only | `cursor` or `offset`, never both | `pages`, `total`, `offset`, `hasMore`, plus `isDone` and a signed `continueCursor` |
| Unpaginated | No `cursor` or `limit` | Named array containing the complete set or the operation's bounded roster |
For keyset lists, send `continueCursor` back unchanged. Do not decode or increment it. At the last page it is empty; sending that empty value again returns `400 INVALID_QUERY`, not the first page. Each operation declares its limit ceiling: usually 100 or 200, and 500 for task comments. Larger query limits are clamped.
Contacts and products are ordered by `updatedAt`, then `id`, both descending. Editing a record while you page can move it ahead of your cursor, so that pass may miss the change. For a full reconciliation, compare each record’s `updatedAt` and repeat complete passes; a cursor does not provide a snapshot of a changing directory. Hub documents, projects, project files, and websites use newest-first `createdAt` order.
Contacts, products, documents, knowledge entries, threads, messages, websites, and notification exports put their keyset rows under `page`. Runs, deliveries, task comments, projects, and project files use their resource name, such as `runs` or `files`. Run lists are newest first, default to 50, and accept limits of 1–200. `GET /api/v1/runs` lists accessible runs across automations.
Automations, agents, skills, folders, models, browser sessions, versions, and triggers are unpaginated. A website-page request with both `cursor` and `offset` receives `400 INVALID_QUERY`; the signed next-offset cursor also works with the normal keyset loop.
## Authentication
Create keys in **Settings > API > REST** as an Owner, Admin, or Developer, or as a member an Admin granted a competence that is used with a key (`tale:models.api`, `tale:notifications.export`, or `tale:rest.act-as`); anyone else is refused with `403 API_KEY_CREATE_FORBIDDEN`; [API keys](https://docs.tale.dev/platform/admin/api-keys) explains the UI. A key appears once and acts as the user who created it. This REST surface does not create, list, rotate, or revoke keys.
| Header | Rule |
| --- | --- |
| `Authorization: Bearer ` | The only supported API-key location; preserve the whole opaque string, including its `tale` prefix |
| `X-Organization-Slug: ` | Select a current membership; always send it in reusable integrations |
| `x-api-key` | Rejected with `401`, even beside a valid Bearer header; it cannot turn an API key into an app session. On the Anthropic-compatible [model endpoint](https://docs.tale.dev/develop/api-reference#model-endpoints), the refusal says to pass the key as an auth token instead: `ANTHROPIC_AUTH_TOKEN` for Claude Code, `authToken` or `auth_token` for the Anthropic SDKs, with `ANTHROPIC_API_KEY` unset (contract 3.6.0) |
A user with exactly one organization can omit the organization header. With several memberships, every request needs it, including reads. The dashboard's selected organization never selects API scope. Slugs are matched without regard to case; blank or whitespace-only values count as absent.
| Organization selection | Result |
| --- | --- |
| Several memberships, no slug | `400 ORG_SLUG_REQUIRED` |
| Unknown slug, or a value that cannot be a slug | `404 ORG_SLUG_INVALID` |
| Existing organization without membership, or with a disabled one | `403 ORG_FORBIDDEN` |
| Valid membership | Request proceeds under that organization and role |
Each of the three refusals lists the organizations you can select in `data.organizations`, as `slug` and `name` pairs. Disabled memberships are left out, so the list is empty when none remains. Retry with one of the listed slugs.
`GET /api/v1/me` also returns the membership list as `organizations`. Its `key.expiresAt` is epoch milliseconds, or `null` for a non-expiring key: rotate unattended credentials before expiry causes `401`. `key.name` identifies the credential in use.
Check both the role and the resource scope before offering an operation. Project readers can chat, comment, and create tasks, and they can change and start the tasks they created or are assigned to; changes to other resources and to other people's tasks require edit access.
| Capability returned by `/me` | What it permits |
| --- | --- |
| `developer` | The Owner, Admin, and Developer roles can start arbitrary live runs, cancel or delete runs, bind or unbind triggers, delete automations, and install or uninstall project automations. These REST operations return `403 ROLE_FORBIDDEN` without it. MCP also checks this capability for saving, deploying, and other privileged tools, using its own error envelope. Validation and mock tools remain available to members. Project access is checked separately. |
| `deploymentEditor` | The operator allowlist permits browser-session import and revocation. An administrative role alone does not grant this capability. |
| `notificationExport` | The key may export members’ notifications through `GET /api/v1/notifications/sync`. Owners and Admins have it through their role; any other member only while an Admin’s `tale:notifications.export` grant is live — see [Delegate the export without an Admin role](https://docs.tale.dev/develop/api-reference#delegate-the-export-without-an-admin-role). Without it, the export returns `403 ROLE_FORBIDDEN`. |
| `skillPublish` | The key may share a skill with the whole organization through `PUT /api/v1/skills/{slug}`. Every member may while the organization has no skill sharing policy; under one, only the roles it admits and members holding a live `tale:skills.publish` grant — see [Save and synchronize skill bundles](https://docs.tale.dev/develop/api-reference#save-and-synchronize-skill-bundles). Without it, such a save returns `403 SKILL_PUBLISH_FORBIDDEN`. |
| `actAs` | The key may name an `actor` — the verified member a relayed gesture is recorded for — on `POST …/runs/{runId}/asks/{askId}` and `POST …/tasks/{taskId}/review`. Owners and Admins have it through their role; any other member only while an Admin’s `tale:rest.act-as` grant is live — see [Name the member the gesture is for](https://docs.tale.dev/develop/api-reference#name-the-member-the-gesture-is-for). An `actor` sent without it returns `403 ROLE_FORBIDDEN`. |
| `modelApi` | The key may call the [compatible model endpoints](https://docs.tale.dev/develop/api-reference#model-endpoints): the organization has turned them on, and the key holder may call them. Owners, Admins, and Developers may through their role; any other member only while an Admin's `tale:models.api` grant is live. Without it, those routes answer `403 MODEL_API_DISABLED` or `403 MODEL_API_FORBIDDEN` (contract 3.6.0). |
## What every request is held to
### JSON and query validation
Send JSON encoded as UTF-8. Invalid UTF-8, NUL characters, unpaired UTF-16 surrogates in keys or values, and integer values beyond 2^53 − 1 return `400 INVALID_BODY`. Represent large identifiers as strings. Nested validation issues include the full field path, such as `messages.0.createdAt`. IDs are strings and timestamps are epoch milliseconds. A timestamp you send is a whole number of milliseconds from `0` to `8640000000000000` (275760-09-13, the latest instant a JavaScript `Date` can hold); any other value returns `400 INVALID_BODY`. A skill’s `updatedAt` records when its `SKILL.md` was written.
| Input | Rule |
| --- | --- |
| Unknown body key | `400 INVALID_BODY`, with the key named in the issue |
| Duplicate JSON key | Last value wins |
| Unknown, repeated, or blank query parameter | `400 INVALID_QUERY` |
| Query on a write operation | Rejected; writes take no query parameters |
| Query `limit` outside its range | Clamped to the operation's range |
| Body number outside its range | `400 INVALID_BODY`; search `limit` and `maxOutputTokens` are examples |
| `Content-Type` | Bodies are parsed as JSON regardless of this header; this surface does not return `415` |
| `Accept` | JSON operations return JSON even when this header requests another format or excludes JSON; there is no `406` |
### Request-size and arrival limits
| Body | Maximum |
| --- | --- |
| Default JSON request | 1 MiB |
| Inline document content | 32 MiB |
| Model endpoint request | 32 MiB |
| Contact bulk import | 8 MiB |
| Conversation snapshot | 8 MiB |
| Staged conversation upload | 30 MiB |
| Skill save | 4 MiB |
| Delivery claim, failure report, or acknowledgement | 64 KiB |
An oversized body receives `413 BODY_TOO_LARGE`. When the declared length exceeds the cap, the platform rejects before reading the body; otherwise it stops at the first chunk over the cap. It never buffers the complete oversized body. Upload-specific rules can be lower than these transport limits.
Headers and body must finish arriving within 15 minutes. A 30 MiB body needs roughly 35 KB/s to meet that deadline. A slower request receives `408 REQUEST_TIMEOUT` and the connection closes. Use a faster link, smaller supported requests, or the two-step project upload, which transfers file bytes outside this JSON arrival window.
### Methods and response identifiers
Existing read routes accept `HEAD`, with the uncompressed `GET` length and no body; `HEAD` is never compressed. `OPTIONS` is keyless and returns `204` with `Allow`. An unsupported verb on an existing route returns `405 METHOD_NOT_ALLOWED` and its allowed verbs. One trailing slash is tolerated.
The production REST surface is server-to-server and does not enable CORS. Keep API keys behind your own backend. The keyless status JSON is a separate CORS-enabled surface.
Every API response includes `X-Request-Id`. To correlate a request with logs, send up to 255 characters drawn from letters, digits, `_`, `-`, and `=`. An invalid value is replaced by a fresh UUID; the response contains the value actually used. `429`, `500`, `413`, and `414` also include `requestId` in the JSON envelope. Two refusals are the exception, because the edge's HTTP parser answers them before a request exists to log: the bare `431` for headers over the 64 KiB budget and the bare `400` for a control character in a header value carry no `X-Request-Id`, no envelope and no `X-Tale-Api-Version` — there is nothing to quote, and the request itself is what to change.
`Idempotency-Key` is read only by the operations that declare it — a run start, a chat send; the OpenAPI document lists them, and the webhook endpoints read it as a delivery id under their own rule. Any other operation ignores the header: its at-most-once guard is the natural key its body names — a contact's `externalId`, a task's `(externalSystem, externalId)`, a project's `externalItemId`. An `Expect: 100-continue` draws one `100 Continue` from the edge as soon as it starts forwarding the body; the platform's own verdict — a `413` for a declared length over the cap — still arrives before a body byte is read.
Responses from `/api/v1` and webhook routes include `X-Tale-Api-Version`; see [Versioning](https://docs.tale.dev/develop/api-reference#versioning). Keyless `/api/health`, `/status`, `/status.json`, and `/openapi.json` do not implement that contract and have no version header.
The URL, including query, is limited to 32 KiB; a larger one gets `414 URI_TOO_LONG` before route lookup. Request headers have a 64 KiB edge budget. HTTP/1.1 permits a few KiB of slack before a bare `431`, so a 66 KiB URL can still reach the platform and receive `414`. HTTP/2 enforces the header budget exactly by closing the connection without a response.
Header control characters below 0x20, except tab, and DEL are rejected before the platform: HTTP/1.1 receives a bare text `400` with no `X-Request-Id`; HTTP/2 resets the stream or closes a connection carrying a body.
Malformed HTTP/1.1 chunk framing, such as a non-hexadecimal chunk size or missing CRLF, receives `400 BODY_CHUNK_MALFORMED` at the edge. Correct the HTTP client or intermediary that encoded the request; retrying the same malformed bytes will not help.
For an oversized declared body, the HTTP/1.1 edge may drain up to 256 KiB before delivering the refusal, although the platform reads none. If a body ends before its declared length, HTTP/2 returns `400 BODY_LENGTH_MISMATCH`, unless the platform's over-cap `413` won the race. HTTP/1.1 waits for the missing bytes until the 15-minute arrival deadline.
Edge refusals have their own request ID and no `X-Tale-Api-Version`: the proxy does not know the application contract. Examples include a dot-segment `404`, `BODY_LENGTH_MISMATCH`, `BODY_CHUNK_MALFORMED`, and `502`/`503`/`504 UPSTREAM_UNAVAILABLE` during a restart. A `503 DATABASE_UNAVAILABLE` is the platform's own answer while its database restarts, so it carries the platform's request ID and `X-Tale-Api-Version` like any other platform response. Do not assume every intermediary response has the API's JSON envelope.
## Caching, compression and partial reads
### Reuse unchanged responses
Every JSON read — a `GET` that returns **200** — carries an `ETag` computed over its bytes and `Cache-Control: private, no-cache`: keep the answer, and send the tag back as `If-None-Match` on the next read. An unchanged resource returns **304** with no body, so a poller that watches a finished run, an idle thread or a document's indexing state spends a round trip instead of the payload.
Send the tag back exactly as you received it: behind the compressing edge the tag of a compressed answer reads `"…-gzip"` or `"…-zstd"`, and that form matches, as does the weak `W/"…"` form; the 304 carries the tag the API computed. File content — `GET /api/v1/projects/{id}/files/{documentId}/content` and a Hub document's `GET /api/v1/documents/{id}/content` alike — honours `If-None-Match` and `If-Modified-Since` against the `ETag` and `Last-Modified` it issues, the same way — a mirror re-downloads a file only when its bytes changed.
The date is judged at the whole-second precision an HTTP date carries, and `If-None-Match` decides alone when both travel; on a content-only Hub document `Last-Modified` is the document's own `updatedAt`, which a title or metadata patch moves too, so a mirror that tracks bytes sends the `ETag`. A **304** still counts as one request against the [rate limits](https://docs.tale.dev/develop/rate-limits).
```bash
# The first read answers 200 and its ETag; the repeat with that tag answers 304
curl -sS --compressed -D - -o /dev/null "https://your-host.example.com/api/v1/projects//runs/?fields=status,finishedAt" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $TALE_ORG_SLUG" \
-H 'If-None-Match: ""'
```
### Request compression
JSON and text responses are compressed when the request offers `gzip` or `zstd` in `Accept-Encoding` — `curl --compressed` does, and every HTTP library can — above a floor of about 512 bytes; `br` is not served. A compressed answer's `Content-Length`, when present, is the compressed size (a large answer streams without one) and its `ETag` reads with the `-gzip`/`-zstd` suffix, as above; a `HEAD` is never compressed and reports the uncompressed length. Request compression for large JSON responses when your HTTP client supports it. The benefit depends on the response content; it does not reduce the number of requests charged to your rate budget.
### Read only the fields you need
Where a resource is large and a read needs only part of it, the operation says so: a run read takes `?fields=status,finishedAt` (any keys of the run, comma-separated) and returns exactly those keys, and a run listing that inlines full rows through `?include=` reads at most 25 rows per page and returns at most 8 MiB of them — it ends at the last row that fits, `isDone: false`, with a `continueCursor` at that row, so keep following the cursor until `isDone`.
## Sign in to an application with Tale
Tale is also an OpenID Connect issuer. A registered application sends you through Tale's native login and consent; it receives a signed identity with a verified email and membership in the one organization bound to its client. An API key does not authenticate a person for this flow.
Register the application with an active Owner or Admin session whose selected organization equals `TALE_ORG_ID`. `TALE_ORIGIN` is your Tale origin and `TALE_SESSION_COOKIE` is that session's cookie header. Use the application's exact HTTPS callback; HTTP is accepted only on loopback for local development:
```bash
curl -sS --compressed -X POST "$TALE_ORIGIN/api/app/identity/clients?orgId=$TALE_ORG_ID" \
-H "Cookie: $TALE_SESSION_COOKIE" \
-H "Origin: $TALE_ORIGIN" \
-H "Content-Type: application/json" \
-d '{"key":"office-app","name":"Office application","redirectUri":"https://office.example.com/api/auth/oauth2/callback/tale"}'
```
The first response is **201** with `{ "created": true, "client": { "client_id": "…", "client_secret": "…", … } }`. Store the secret in the application's secret environment. Repeating the same key and configuration returns **200**, `created: false`, and the same client ID without the secret. A changed callback or policy returns **409** so a rerun cannot silently redirect an existing integration.
| Purpose | Endpoint or requirement |
| ---------------- | ---------------------------------------------------------------------------- |
| Issuer | `https://your-host.example.com/api/auth` |
| Discovery | `GET /api/auth/.well-known/openid-configuration` |
| Authorization | `GET /api/auth/oauth2/authorize` |
| Code exchange | `POST /api/auth/oauth2/token`, `client_secret_basic` or `client_secret_post` |
| Signing keys | `GET /api/auth/jwks` |
| Current identity | `GET /api/auth/oauth2/userinfo`, bearer access token |
| Requested scopes | `openid profile email tale:organization` |
Use a maintained OIDC client with authorization code flow, S256 PKCE, one-use state and a nonce. Validate the issuer, audience, RS256 signature, expiry and nonce of the ID token, then require `email_verified: true`. The `https://tale.dev/organization` claim contains `{ "id", "slug", "role" }` for the registered organization. Every ID token also carries `acr: "0"`. Discovery lists `"0"` under `acr_values_supported` and `acr` under `claims_supported`. Tale asserts no stronger authentication context, MFA enforcement included, so do not gate on `acr`. Releases from v0.5.45 documented `urn:mace:incommon:iap:bronze` while the token already carried `"0"`; a relying party pinned to the URN must accept `"0"`. Discovery's `prompt_values_supported` names what the issuer honours — `none`, `login`, `consent`; `select_account` and `create` are not offered.
The ID token carries the standard claims of the scopes you request, with the values userinfo returns, so you can read them without a userinfo request. `email` adds `email` and `email_verified`. `profile` adds `name`, split into `given_name` (every word but the last) and `family_name` (the last word) when the name has two or more words, and `picture` when the account has one.
Tale rechecks current membership and native MFA enforcement before issuing tokens and when reading userinfo; the application remains responsible for its own account access policy. Codes, access tokens and ID tokens expire after five minutes, and a code can be redeemed once. Dynamic registration, implicit grants and refresh tokens are disabled.
Access tokens serve only native userinfo; external resource audiences are disabled. Use native API keys for REST requests.
### Handle identity-provider errors
Errors follow RFC 6749 and RFC 6750 — what a maintained client expects. `userinfo` returns **401** `invalid_token` with a `WWW-Authenticate: Bearer` challenge for an invalid or expired access token — every five-minute expiry walks this path, so treat it as a sign-in, not a retry — and **401** with the bare challenge when the token is missing; a token without the `openid` scope returns **403** `insufficient_scope`.
The token and authorization endpoints answer `{ "error", "error_description" }`: a grant other than `authorization_code` is `unsupported_grant_type`, a malformed request — a missing `grant_type` included — `invalid_request`, with the description naming the parameter (`grant_type is required`, `client_id is required`, `response_type must be one of "code"`). A body of the wrong media type on a JSON-only endpoint (a form posted to `register`) returns **400** `invalid_request` naming the type it takes — there is no 415 on this surface.
An authorization request whose `client_id` names no registered client is redirected to the issuer's own error page — never to the `redirect_uri` it sent — with `error=invalid_client` and a description that says the client is unknown, so a mistyped id is not mistaken for a missing one. Discovery lists `https://tale.dev/organization` under `claims_supported`; it also advertises the provider's introspection, revocation and end-session endpoints, which the flow above does not need.
### Rotate or disable an application client
For a reviewed client key, `POST /api/app/identity/clients/office-app/rotate-secret?orgId=` with `{}` returns a new `client_secret` once and retires the old secret. `POST /api/app/identity/clients/office-app/status?orgId=` with `{ "disabled": true }` blocks new authorizations; `false` restores the same client. Both require the same current organization, administrator session, Origin header and JSON content type as registration. Deleting an organization removes its clients and consent grants.
## Endpoint groups
For a project resource under `/api/v1`, put its project ID in the URL. These request bodies do not accept `projectId`; strict schemas reject it with **400**. The resource must belong to the named project and be visible to the key holder, otherwise the call returns **404**. Responses may include `projectId` as resource metadata. Organization catalogs, such as automation definitions and skill bundles, keep their organization paths.
Every **201** that creates one addressable resource carries `Location` — the resource's own path, relative to the request URL — so a generic client follows it whatever shape the body has (`{id}` on a contact, `{project}` on a project, `{task}` on a task); the bulk contact import creates many and carries none.
Automation authoring is separate from this REST surface. Use the [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) or the app’s editor to save, validate, test, and deploy definitions. `tale deploy` releases deployment configuration; it is not a REST authoring endpoint.
| Resource | Route and scope |
| --- | --- |
| Automations | `/api/v1/automations/...`
Organization definitions, versions, triggers and the projects each is installed in; delete a definition; start and list runs that have no project. |
| Project automations | `/api/v1/projects/{id}/automations/...`
List installed automations, install or uninstall one, start and list this project's runs. |
| Runs | `/api/v1/runs`, `/api/v1/projects/{id}/runs`, and one run at `/api/v1/projects/{id}/runs/{runId}` or `/api/v1/runs/{runId}`
List runs across automations; read one in full — status, output, trace, effects; `POST .../cancel` a live one and `DELETE` a finished one; read the question a waiting run asks at `GET .../ask` and answer it at `POST .../asks/{askId}`; use the project path for a project run. |
| Threads | `/api/v1/projects/{id}/threads/...` or `/api/v1/threads/...`
The key holder's project chats or chats with no project: list, create, read, archive or restore, delete, send messages, poll the turn and cancel it. |
| Models | `GET /api/v1/models`
Configured chat models available to the key holder in this organization — plus `harnesses`, the coding harnesses a project agent may run on — with `contextWindow`, `maxOutputTokens` (absent when the catalog declares no ceiling — then no cap check applies to a send), capabilities, optional `pricing` when the catalog publishes rates, and `default: true` on the organization's pick when one is configured and accessible. |
| Teams | `GET /api/v1/teams`
Every team of the organization — `id`, `name` and whether the key holder is a `member` — as a complete set: the ids a team audience takes (`teamIds` on a project or a Hub document, `teams` on a skill). Teams are created and staffed in the app (Settings > Teams) or by an identity provider; nothing on this surface writes one. |
| Agents | `/api/v1/projects/{id}/agents/...`
List, read, create, update (conditionally, with `expectedUpdatedAt`) and delete agents within the required project. A `PUT` that names the configuration already stored writes nothing and leaves `updatedAt` alone. |
| Skills | `/api/v1/skills/...`
List, read, create or update, and delete organization skill bundles; read any file of a bundle — a validated read (`ETag` and `Last-Modified` on the bytes; `If-None-Match` / `If-Modified-Since` answer **304**), so a mirror re-downloads only what changed. Every skill names its version (`etag`, `updatedAt`) and a save can be guarded with `If-Match`; a skill keeps no version history on this surface — the read answers the current bundle only. |
| Knowledge entries | `/api/v1/knowledge-entries/...`
Topic-keyed facts: list (`?topic=`, `?status=`), create, supersede, delete, and one topic's version history at `GET .../{id}/versions`. |
| Knowledge search | `POST /api/v1/projects/{id}/knowledge/search` or `POST /api/v1/knowledge/search`
Search one project's indexed files, or visible non-project Hub documents and websites. |
| Documents | `/api/v1/documents/...`
Knowledge-base documents: CRUD (a `PATCH` returns the updated document), `GET .../content` for the bytes, plus `POST .../retry-indexing`; every file-backed document carries its `indexing` state. Hub only — project files live under Projects. |
| Websites | `/api/v1/websites/...`
Crawled sources: CRUD (a `PATCH` returns the updated website) plus `.../pages` — each page with its `status` (`discovered` until a fetch stores it, `active` from then on), `failCount`, and, when its last attempt failed, `lastError`, `lastErrorKind` and `lastErrorAt`, so a page the fetch guard refused (a redirect into a private address) is told apart from one nobody has fetched yet; `?state=failed` or `?state=skipped` narrows the window to the pages in that state (`total`, `hasMore` and the cursor then count that state alone, and a cursor minted under one state is refused under another), and every window carries `counts`, the failed and the skipped pages' numbers — `.../sync` and `.../search`. Search returns `{results, total}` — each result its `url`, `title`, `content`, `chunkIndex` and `score` — a shape of its own, not the `{hits, diagnostics}` of the knowledge-search endpoints; its `limit` (1–100, default 10) is a body field, refused with **400**, `INVALID_BODY`, when out of range — never clamped. On the website, `crawledPageCount` counts the pages the crawler attempted, stored or not, and `failedPageCount` those whose last attempt failed; the three counts are stamped by the corpus → row sync, which runs after discovery, after every stored batch, at each link's end and at the scan's end (`metadata.lastStatusSyncAt` says when) and which `POST .../sync` forces, and `lastScannedAt` is when the last scan ended. Discovery honours `robots.txt` `Disallow` rules on every path a URL can enter by, listed URLs excepted, and a page a rule covers leaves the index on the next scan; `POST /api/v1/websites` refuses an `http://` domain (`WEBSITE_DOMAIN_INVALID` — the crawler dials https only) and drops a trailing dot, so `example.com.` is `example.com`; a page's `lastError` is one line naming the cause, never a framework call log. |
| Browser sessions | `/api/v1/browser-sessions/...`
Browser cookies for [video ingestion](https://docs.tale.dev/self-hosted/configuration/video-ingestion). Organization members can read the masked list. `POST .../import` and `DELETE .../{id}` require an allowlisted deployment editor; check `capabilities.deploymentEditor` in `/me`. Sessions last 14 days by default and at most 180 days. |
| Products | `/api/v1/products/...`
Product catalog entries: CRUD (a `PATCH` returns the updated product). |
| Contacts | `/api/v1/contacts/...`
Contact records: CRUD (a `PATCH` returns the updated contact) plus `POST /api/v1/contacts/bulk`. |
| Conversations | `/api/v1/conversations/...`
Mirror external conversations into Inbox as versioned snapshots, read a source's snapshot receipt, peek at a source's delivery queue, claim native replies, acknowledge or fail their delivery, re-drive a dead-lettered one, and queue a mirrored conversation to a team; exact schemas are in the running instance’s `/docs`. |
| Notifications | `GET /api/v1/notifications/sync`
Read-only export of one verified member’s personal or organization feed; Owners and Admins, or a member an Admin granted `tale:notifications.export`. Signed pagination, localized text, stable IDs and content/read-state hashes. |
| Projects | `/api/v1/projects/...`
The machine endpoint for external workers: list projects or look one up by external id, create, archive and restore, delete; prepare folders, upload, download and delete files, index a file now, delete folders. |
| Tasks | `/api/v1/projects/{id}/tasks/...`
Idempotent task creation from an external ref, state reads, workflow starts (answering the `runId` to poll), comments, and the task’s review — read at `GET .../review`, decided for a member at `POST .../review` — within the named project. |
| MCP | `POST /api/v1/mcp`
The [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) — same key, JSON-RPC instead of REST. |
| Model endpoints | `/api/v1/openai/...` and `/api/v1/anthropic/...`
OpenAI- and Anthropic-compatible calls to the organization's models, off until an Admin turns them on; see [Compatible model endpoints](https://docs.tale.dev/develop/api-reference#model-endpoints). |
| Webhook trigger | `POST /api/projects/{id}/automations/webhook/{token}` or `POST /api/automations/webhook/{token}`
Start a deployed automation using its token; the [Webhooks page](https://docs.tale.dev/develop/webhooks) covers project and non-project URLs. |
### Avoid overwriting a concurrent edit
Use `expectedUpdatedAt` in a contact, product, or document `PATCH` when your change depends on the last row you read. A stale value returns `409 CONTACT_STALE`, `PRODUCT_STALE`, or `DOCUMENT_STALE`. Reload the resource, merge your intended change with the intervening edit, then submit the new precondition.
For Hub documents, `If-Match` offers a representation-level guard: send the strong `ETag` returned by the document’s `GET`. The comparison runs inside the document’s write transaction. A mismatch returns `412 PRECONDITION_FAILED`, includes the current tag in `data.etag`, and writes nothing. A tag list or `*` is accepted; a weak `W/` tag never matches. The tag covers the whole read, including `indexing`, so indexing progress can invalidate it even when the document row’s `updatedAt` has not changed.
A successful contact, product, document, or website `PATCH` returns `200` with the updated resource; a document `PATCH` also carries the new representation's `ETag`, the value the next `If-Match` sends. For contacts, products, documents, and projects, a patch that leaves all stored values unchanged performs no write and preserves `updatedAt`. Preconditions are checked first: a no-op body does not bypass a stale `expectedUpdatedAt` or `If-Match`.
### Edit contacts, products, and website identity
Contacts and products share one editing vocabulary. Strings are trimmed; a contact's `email` is stored lowercase, so duplicates match case-insensitively, and the part before `@` is at most 64 characters. A second contact with the same `email` or `externalId` returns **409**, `CONTACT_DUPLICATE_EMAIL` or `CONTACT_DUPLICATE_EXTERNAL_ID`; a second product with the same `name` — compared without regard to case — or `externalId` returns **409**, `DUPLICATE_PRODUCT_NAME` or `DUPLICATE_PRODUCT_EXTERNAL_ID`, on create and on a `PATCH` that renames onto either.
`null` clears any optional field, and on `PATCH` a blank string reads as `null` — a blank required field (a product's `name`) returns **400**, `INVALID_BODY`; on create and bulk import a blank string — or `null` — reads as the field left out, so a CSV-shaped row or a JSON export imports cleanly (the OpenAPI document declares the optional create fields nullable for that reason). A bulk import carries at least one row — an empty `contacts` array returns **400**, `INVALID_BODY`, never a **201** that created nothing. A contact is filed under at least one of `name`, `email` and `externalId`: a patch that would clear the last one returns **400**, `CONTACT_IDENTITY_REQUIRED`.
Read a contact bulk-import result row by row. `POST /api/v1/contacts/bulk` accepts an object containing only `contacts`, an array of 1–500 rows. It validates rows independently and returns `201` even when some fail:
| Response field | How to use it |
| --- | --- |
| `success`, `failed` | Counts of accepted and rejected rows |
| `created[]` | Each created contact’s `id` and original zero-based `index` |
| `errors[]` | Original `index` and `contact`, human-readable `error`, stable `errorCode`, and field `issues` for schema failures |
A malformed email, unknown row key, or missing identity produces an `INVALID_BODY` row error; duplicate identities have their own codes. Correct only the failed rows before resubmitting. Invalid batch structure, transport limits, or invalid JSON encoding still reject the whole request before row processing.
The contact list’s `source` filter accepts the same closed source enum as writes, including `manual_import`, `api_import`, `shopify`, `hubspot`, `webhook`, and `custom`. Use the full enum in the instance’s OpenAPI schema; an unknown value returns `400 INVALID_QUERY`.
`PATCH` merges `metadata` per RFC 7396 — contacts, products and documents alike: sent keys are set, omitted keys stay, a key sent as `null` is removed, and the whole field sent as `null` clears it — while `address` is replaced whole, an address being a unit. `address` and `metadata` (a document's `metadata` too) are bounded to 64 KiB of JSON, 8 levels of nesting and 500 keys in total; a larger value returns **400**, `INVALID_BODY`, naming the path.
A product’s `price` and `stock` are numbers within the safe-integer range, negative values included (a correction booked through the API); the app’s product form and file import refuse negatives and a non-integer stock. A product’s `currency` is an ISO 4217 code (`USD`, `EUR`), accepted in any case and stored uppercase. Its `imageUrl` must be an absolute HTTP or HTTPS URL with a public host. Relative paths, other schemes, private or loopback IP literals, single-label names, and cloud metadata hosts return `400 INVALID_BODY`. Validation examines the URL without a DNS lookup and does not fetch the image. The operator setting `TALE_ALLOW_PRIVATE_CRAWL_HOSTS=1` allows private-network targets but never metadata hosts.
An image uploaded through the product form is a separate case. The app accepts PNG, JPEG, WebP, GIF, or SVG files up to 5 MiB, checks their bytes, and returns a stable protected URL. REST product reads expose that address as an absolute URL. You may send it back in `imageUrl`, including on a private deployment, if it belongs to this organization and you uploaded it or a current product already uses it. An inaccessible or missing image returns `404 FILE_NOT_FOUND`; an altered managed URL returns `400 INVALID_BODY`. Viewing its bytes requires an authorized app session; the URL is not a public sharing link, and a REST API key does not grant access to this app route. Set `imageUrl` to `null` on PATCH to remove the product’s image. Uploading image bytes uses the app form, not a REST product upload endpoint.
A website's `domain` is immutable: `PATCH /api/v1/websites/{id}` accepts the stored value echoed back (a client may send the resource it read) and returns **400**, `WEBSITE_DOMAIN_IMMUTABLE`, for any other. `POST /api/v1/websites` stores the host as given — `www.` is kept — and the `www.` and apex spellings count as one site: a domain already registered under either spelling returns **409**, `WEBSITE_DUPLICATE_DOMAIN`, with `data.websiteId` and `data.domain` naming the existing row.
The one exception is a URL list posted onto a domain registered as a list under the same spelling, which extends it and returns **200** with the existing id; a list posted onto a whole-site crawl is the same **409** — the crawl keeps its kind and nothing is queued — so read `kind` before you post, or re-post under the spelling the 409 names.
A website’s `status` describes its scan lifecycle. Filter with `GET /api/v1/websites?status=` using `scanning` (a registered site starts here), `active`, `error`, or `deleting`; other values return `400 INVALID_QUERY`, and so does `?scanInterval=` outside its seven values. `active` means a finished scan has stored at least one page, not that every page was refreshed. `error` means the scan failed or no pages remain stored after attempted fetches; inspect `metadata.lastSyncError`. A failure of the embedding model opens with `The embedding model could not embed the pages []:`, where the class is one of `credential`, `credit`, `unresolved`, `dimension`, `throttled` or `upstream`, followed by the provider's own sentence.
Each page’s `lastErrorKind` helps distinguish a network or TLS failure (`network_error`, `tls_error`), a refused target (`private_ip`), an HTTP failure (`http_error`), extraction or rendering failures, content that cannot be turned into text (`unsupported_content`), and a `robots_noindex` refusal — the origin answered `X-Robots-Tag: noindex` or the page carries a `` tag. A failed refresh can retain previously indexed content, so inspect page failures alongside the site’s status.
`POST /api/v1/websites/{id}/search` performs keyword search over that site’s stored chunks. Its BM25 `score` is unbounded and comparable only within one response. Deployments without ParadeDB use substring matching and report `0` for every hit. For semantic similarity and `minSimilarity`, use `POST /api/v1/knowledge/search` with `corpus: "web"`; that searches the visible web corpus, not one selected site.
### Save and synchronize skill bundles
`PUT /api/v1/skills/{slug}` creates the skill when the slug is free — **201** — and updates it in place otherwise — **200** — so the status is the create-or-update signal, the same convention as materializing a task, and a sync that mirrors bundles from elsewhere needs no read first: `description` and `body` are required, an omitted `icon`, `labels`, `teams`, `visibility` or `disableModelInvocation` keeps its stored value, `null` clears `icon` or `labels`, and `disableModelInvocation: false` drops the flag.
The body is Markdown of at most 507,893 bytes of UTF-8 — bytes, not characters: the composed `SKILL.md`, frontmatter included, is capped at 512 KiB and this budget always fits inside it — and a body that does not end with a newline gets one appended, so a `GET` reads it back one byte longer. The save rewrites `SKILL.md` only — every other file of the bundle stays, and frontmatter keys the body does not carry (`license`, `recommended-packages`, community keys) are preserved; replacing a whole bundle is the app's zip upload.
Every skill names its version: `etag`, the quoted SHA-256 of its `SKILL.md`, and `updatedAt`, when that file was last written — the tag moves with every save of the document and with nothing else.
A save whose composed `SKILL.md` is byte-identical to the stored one writes nothing: it returns **200** with the stored `etag` and `updatedAt` and adds no history entry — the no-op documents and knowledge entries already follow — so a mirror that re-pushes an unchanged bundle leaves `updatedAt` a change signal; the preconditions are still evaluated first, so a stale `If-Match` on an identical body is still **412**.
`GET /api/v1/skills/{slug}` carries the tag as `ETag` and returns **304** to an `If-None-Match` that names it — the weak `W/"…"` and the edge's `"…-gzip"` forms included — with the same `Cache-Control: private, no-cache` as its **200**. `GET /api/v1/skills` also returns `failures` — bundles on disk that could not be read, each with `slug`, `path` and `message`; normally an empty array — so a listing never fails because one bundle is broken.
Guard an update or a delete with `If-Match`: the `etag` you last read — a skill whose document changed since returns **412**, `SKILL_STALE`, with the current tag in `data.etag`, and nothing is written, so reload and merge before saving again (a guarded `PUT` of a skill that is not there is the same **412** with `data.etag: null`, while a guarded `DELETE` of one answers the plain **404**, `SKILL_NOT_FOUND` — already gone is done); a weak tag (`W/"…"`) never matches, and the body is validated before the precondition is evaluated. Send `If-None-Match: *` to create only: a slug that already has a bundle then returns **412**, `SKILL_EXISTS`, and nothing is written.
`GET /api/v1/skills/{slug}/files/{path}` reads any file of the bundle — `SKILL.md` included — as raw bytes named by `Content-Disposition`, with `path` exactly as `files[].path` lists it (`/` raw or `%2F`); the read is validated — the `ETag` is the file's bytes, `Last-Modified` its modification time, and `If-None-Match` or `If-Modified-Since` answers **304** —; a path the list would never carry returns **404**, `SKILL_FILE_NOT_FOUND`, and a bundle the file layer refuses — a planted symlink, a file over the 4 MiB staging cap — **422**, `SKILL_MALFORMED`.
A raw dot-segment in the URL (`../`, or `%2e%2e/` — the dots encoded, the slash not) never reaches the route: the edge refuses it first with its own **404**, `NOT_FOUND`, an `X-Request-Id` of its own and no `X-Tale-Api-Version`; a spelling whose slashes are encoded too (`%2e%2e%2f…`) reaches the route and reads as `SKILL_FILE_NOT_FOUND`. Every skill carries `canEdit` — whether this key may edit the bundle; shipped skills are organization bundles an administrator may overwrite — so check it before a save that means to replace one. Skills support `org` and `team` visibility; `teams` must name teams in this organization.
Every skill also says who created it and who last edited it. `origin` is `release` for a skill a managed configuration release installed, `builtin` when no creator is recorded (the document skills an organization starts with), and `member` otherwise, with `owner` the creator's user ID. `ownerName` is the creator's display name while they are a member of the organization, and is absent once they have left. `updatedBy` and `updatedByName` name the member whose write through Tale produced the stored `SKILL.md`; they are absent when nobody has edited the skill since it was created, and when its file changed outside Tale since the last edit. A save that changes the skill is recorded in the organization's audit log as the key holder: `skill.created`, `skill.updated`, and `skill.sharing_changed` when `visibility` or `teams` changed (contract 3.4.0).
An organization can reserve organization-wide skills with its [skill sharing policy](https://docs.tale.dev/platform/admin/governance/policies-and-limits#skill-sharing): to Editors and above, or to Owners and Admins, plus members holding a live `tale:skills.publish` grant. A key holder outside that set gets **403**, `SKILL_PUBLISH_FORBIDDEN`, for a save that would create a `visibility: org` skill, widen one to `org`, or change an `org` skill in place; nothing is written, and the refusal is recorded in the audit log as `skill.publish_denied`. Saving with `visibility: team` and the holder's own `teams`, a byte-identical save, and `DELETE` stay open. `capabilities.skillPublish` in `GET /api/v1/me` answers the question before the first save (contract 3.5.0).
`private` skill visibility is retired: it cannot be set, and a bundle that already carries it keeps it only when the save omits `visibility`. A slug is at most 64 characters of lowercase letters, digits and single hyphens, and `anthropic` and `claude` are reserved; `PUT` refuses a malformed one with **400**, `INVALID_SKILL_SLUG`, naming the rule it breaks, while `GET` and `DELETE` answer it as absent with **404**, `SKILL_NOT_FOUND`.
### Mirror conversations and deliver replies
Create the contact with `POST /api/v1/contacts` first. A conversation snapshot links `externalContactId` to that contact's `externalId` in the organization. An unknown contact returns **404** `CONTACT_NOT_FOUND`; an ambiguous match returns **409** `CONTACT_AMBIGUOUS`. A snapshot cannot move an existing source conversation to another contact: changing its `externalContactId` returns **409** `CONVERSATION_CONTACT_CONFLICT`.
`POST /api/v1/conversations/sync` compares the integer `version` with the stored snapshot:
| Incoming snapshot | Result |
| --- | --- |
| Newer version | Apply it. |
| Older version | Ignore it. |
| Same version, different content | **409** `CONVERSATION_SNAPSHOT_CONFLICT`. |
| `deleted: true`, version equal to or newer than stored | Close the mirror. Repeating this after closure changes nothing. |
Closing preserves the conversation and messages in the Inbox. This API does not hard-delete them.
`GET /api/v1/conversations/sync` also reports the linked `externalContactId`, the bound row's `contactId`, and `contactStatus`: `active`, `trashed`, or `missing` — and `sourceDeleted` with the Inbox `status`, so an engine that resumes from the receipt knows a teardown landed: a content snapshot onto a torn-down mirror answers **409** `CONVERSATION_CLOSED` at any version (mirror the source conversation under a new `externalId` to start again). The binding keeps the original contact row. Deleting that contact moves it to trash and frees its email and external ID, but recreating those identifiers never transfers the old conversation’s history. A contact re-keyed in the CRM (a `PATCH` of its `externalId`) keeps its conversations: a snapshot naming the current id applies and the receipt follows it, while the id it no longer carries returns **409** `CONVERSATION_CONTACT_CONFLICT` naming the id the conversation is bound to. `GET /api/v1/conversations?source=` lists every conversation you mirrored under a source — `conversationId`, `externalId`, `externalContactId`, `contactId`, `contactStatus`, `version`, `sourceDeleted`, `status`, `subject` — newest first as a keyset page under `conversations` (the same `?cursor=` loop as every list), and `?contactStatus=trashed` finds the mirrors a deleted contact froze.
A newer content snapshot for a trashed contact returns `409 CONVERSATION_CONTACT_TRASHED`. Restore the contact — `POST /api/v1/contacts/{id}/restore`, which applies the create's own rule (a live contact that has since taken its email or `externalId` refuses the restore with the create's **409**), or the app's trash — before sending more content, or close the mirror with `deleted: true` at an equal or newer version; a closed mirror is not reopened by the restore, so a later content snapshot answers the same 409 while the contact stays in the trash. Older snapshots remain ignored, and same-version repeats still follow the version rules above; deleting the contact does not turn every replay into an error.
Claim replies written in the Tale Inbox through `POST /api/v1/conversations/deliveries/claim`. Before including one in a later source snapshot, acknowledge its delivery under its `externalId`, then set `taleMessageId` to the reply's `messageId`. An unacknowledged reply returns **409** `DELIVERY_UNACKNOWLEDGED`. Without `taleMessageId`, a message is treated as originating in the source system, regardless of `isCustomer`.
A claim must name a source you have mirrored. An unknown source returns **404** `CONVERSATION_SOURCE_NOT_FOUND`; one owned only by other service users returns **403** `INTEGRATION_NOT_OWNED`. A source typo therefore produces an error rather than an apparently healthy empty queue.
A claimed delivery includes `attempts`, `leaseExpiresAt`, `lastErrorCode`, and `firstClaimedAt`. Use `GET /api/v1/conversations/deliveries?source=` to inspect the queue without claiming replies. It returns status (`queued`, `leased`, `failed`, `delivered`), attempt counts, and timestamps, but no claim token or message body. Entries are ordered oldest-due first in `{deliveries, isDone, continueCursor}`; pass `?cursor=` for subsequent pages.
Filter with `?status=failed` to find dead-lettered deliveries. `POST /api/v1/conversations/deliveries/{id}/retry` retries one and records the same audit action as **Retry** in the Inbox. A delivery that is not dead-lettered returns **409** `DELIVERY_RETRY_UNAVAILABLE`.
Queue a mirrored conversation to a team with `POST /api/v1/conversations/assignment`: `{source, externalId, teamId}`, where `teamId` comes from `GET /api/v1/teams` and `null` clears it. The team's members can then open the conversation in the Inbox, and each of them is notified. As in the Inbox, only admin and owner keys may assign; an editor key returns **403** `ROLE_FORBIDDEN` and can route its source with a [conversation routing rule](https://docs.tale.dev/platform/admin/governance/policies-and-limits) instead. A team outside the organization returns **400** `TEAM_NOT_IN_ORG`, a mirror no snapshot created returns **404** `CONVERSATION_NOT_FOUND`, and one another service user owns returns **403** `INTEGRATION_NOT_OWNED`.
Stage attachments through `POST /api/v1/conversations/uploads` before referencing them in a snapshot. Invalid attachments prevent the snapshot from being applied:
| Problem | Response |
| --- | --- |
| Unstaged, expired, malformed, or cross-organization `storageId` | **400** `ATTACHMENT_NOT_STAGED`. |
| Attachment staged by another service user in the organization | **403** `ATTACHMENT_NOT_OWNED`. |
| Declared `size` differs from the uploaded bytes | **400** `ATTACHMENT_SIZE_MISMATCH`. |
`replyConstraints` limits message length, attachment count, attachment size, and file extensions for replies a person writes in the Inbox. These limits apply when composing a reply, not when accepting a source snapshot.
`GET .../deliveries/{id}/attachments/{index}` returns `DELIVERY_NOT_FOUND` if the claimed delivery does not exist. A missing attachment position, or an index outside the integers 0 through 9, returns `ATTACHMENT_NOT_FOUND`.
There is no staged-upload delete route. Unbound uploads become eligible for cleanup after their two-hour window plus a 24-hour grace period; cleanup runs on a later upload in the organization. Bound attachments follow their message's lifetime. All conversation request bodies are strict: unknown keys, including those inside messages and attachments, return **400** `INVALID_BODY` with the field name.
### Create searchable knowledge and Hub documents
`POST /api/v1/documents` can store text directly as `content`. This inline content remains readable but is not indexed. Knowledge search finds only file-backed documents, so `POST .../retry-indexing` returns `{"status": "skipped", "reason": "content-only"}` for an inline document.
Other skip reasons are `untracked-blob`, `unsupported` (terminal indexing failure; inspect `indexing.errorCode`), and `in-progress` (a fresh indexing job is already queued or running). For `in-progress`, poll the document. Retrying a file that previously opted out of indexing opts it back in and returns `indexing`.
A project file is not this endpoint's — it returns **404**, `DOCUMENT_NOT_FOUND`, like any id outside the Hub — but has the same retry at `POST /api/v1/projects/{id}/files/{documentId}/retry-indexing`, which lifts the bind-time `skipRagIndexing` opt-out (see **Verify what landed** below). Either retry runs through the same guards as the app's **Index now**, a budget of 10 per user per minute included (**429**, `RATE_LIMITED`). Its `fileId` alternative requires the key holder's own unbound Hub upload in the selected organization, created through the app; REST does not mint one.
Use `POST /api/v1/knowledge-entries` to create searchable text. It creates a file-backed Hub document with `sourceProvider: knowledge` and starts indexing. An entry allows at most 8,000 characters, with one active entry per topic. The **201** response `{id, documentId}` already contains the document ID: poll `GET /api/v1/documents/{documentId}` to follow indexing.
A `PATCH` supersedes the active entry and re-indexes under the same `documentId`. If `topic` and `content` are unchanged after trimming, it creates no version and returns the existing entry ID. A `PATCH` of a superseded row returns **409**, `KNOWLEDGE_ENTRY_SUPERSEDED`, naming the topic's active row in `data.activeId` (and its direct successor in `data.supersededBy`) — update that row, no chase down the chain. An entry created or superseded over this door reads `source: "api"` (the app's form writes `manual`, the assistant's capture `chat`), so the Knowledge entries table tells the three apart. Deleting the entry trashes its backing document.
The entry endpoints need the knowledge write grant — a read-only member returns **403**, `KNOWLEDGE_ENTRY_FORBIDDEN` — and an object store that does not accept the content within 30 seconds returns **503**, `KNOWLEDGE_ENTRY_STORE_TIMEOUT`, with nothing written. That document refuses a direct `DELETE /api/v1/documents/{id}`, or a `PATCH` of its title or content, with **409**, `DOCUMENT_HAS_KNOWLEDGE_ENTRY` and `data.entryId` — the entry is the way to change it. `GET /api/v1/knowledge-entries?topic=&status=superseded` lists one topic's replaced versions, each stamped with `supersededAt`, and `GET /api/v1/knowledge-entries/{id}/versions` returns the whole chain from any of its rows, newest first.
`GET /api/v1/documents` lists Hub documents newest first. Choose the folder scope explicitly when reproducing the app’s folder view:
| `folderId` query parameter | Documents returned |
| --- | --- |
| Omitted | All visible Hub documents, including those inside folders. |
| `root` | Only documents that are not in a folder. |
| A folder ID | Documents directly inside that folder. |
Read a document's bytes at `GET /api/v1/documents/{id}/content` — the same download choreography as a project file (`Content-Disposition`, `Range`, `HEAD`), and a content-only document returns its inline text there too, typed as its `mimeType`; `GET /api/v1/documents/{id}` carries `content` only for a content-only document, `null` for a file-backed one. Every document returns `contentHash` — the SHA-256 the platform computed for its bytes (a knowledge entry's content, a synced file), `null` otherwise — as a field of its own, never a key in your `metadata`.
A document `PATCH` that changes nothing — an empty body, or every field already at its value — writes nothing and leaves `updatedAt` alone, so a no-op retry never invalidates another client's `expectedUpdatedAt`; a controlled record's content, MIME type, extension or source provider is refused with **400**, `DOCUMENT_RECORD_FROZEN` (in review or approved) or `DOCUMENT_RECORD_REPLACEMENT_REQUIRED` (a draft — use the replacement flow), and a `teamIds` entry the key holder is not a member of with **403**, `TEAM_ACCESS_DENIED` (a team that is not the organization's is **400**, `TEAM_NOT_IN_ORG`; a repeated id collapses to one).
Every file-backed document carries `indexing` — `status` is `pending`, `queued`, `running`, `completed`, `failed`, `unsupported` or `skipped`, with `indexedAt`, `error` and `errorCode` when set — so poll the document after a create or a `retry-indexing` instead of sleeping. An upload already attached to any document, thread or conversation cannot be reused here. A missing upload, another user's upload or a bound upload returns **404**, `FILE_NOT_FOUND`. Project, chat and conversation uploads cannot become Hub documents through this route. Trashed or expired documents, including files deleted with their project, stay out of this Hub surface. Files you keep when deleting their project are detached and remain in the Hub.
Branch on `indexing.errorCode`, not the wording of `error`. The OpenAPI schema enumerates the closed set:
| Status and codes | Recovery |
| --- | --- |
| `unsupported`: `unsupported_type`, `image_no_vision`, `empty`, `not_text`, `malformed` | Replace or re-export the source in a supported format. For `not_text`, export actual UTF-8 text. `malformed` currently identifies an unreadable PDF; corrupt Office files can instead report `indexer_error`. The retry route skips terminal codes, including older rows still marked `failed`. |
| `failed`: `embedding_upstream`, `indexer_error`, `index_rebuilding` | The background job retries these failures. Poll before requesting another attempt. |
| `failed`: `embedding_not_configured`, `embedding_provider_refused`, `index_repair_failed` | Ask the operator to correct provider configuration, permissions, or index health, then retry. `embedding_provider_refused` also covers a model that answers vectors of another width than the settings state, and an embedding credential the platform cannot use (none configured, deleted, disabled, or unreadable); saving corrected embedding settings, or adding or repairing the credential the embedding model uses, re-queues every document that failed on the embedding model. |
| `failed`: `secret_detected`, `pii_blocked` | Correct the source or the organization’s approved content policy before retrying. |
| `failed` with no `errorCode` | The platform settled the failure without classifying it, usually because the indexing run stopped before it finished (a lost job or a stopped worker). Don't wait for an automatic retry: request `retry-indexing`. |
`POST` and `PATCH` bodies are strict: `projectId` is refused with **400**. Create project files through the project upload and file routes below.
## Mirror a member’s notifications
`GET /api/v1/notifications/sync`, added in API contract 1.8.0, exports the notifications a particular member can see. Use it for a one-way mirror in another application. The caller’s API key must belong to an Owner or Admin of the selected organization, or to a member an Admin granted the `tale:notifications.export` capability (API contract 1.14.0). Any other role alone, Developer included, is insufficient.
### Delegate the export without an Admin role
A mirror worker does not need an Admin account. The Admin role also manages members, administers single sign-on and SCIM, and can reset lower-ranked members’ passwords, so run the worker as an ordinary member and grant that member the one capability the export checks. The grant is an entry in the organization’s competence register: it applies only in that organization, is audited, can carry an expiry, and is revoked automatically when an Admin removes the member or SCIM deprovisions them.
An Owner or Admin grants it under **Settings > Governance > Competences** ([Competences](https://docs.tale.dev/platform/admin/governance/competences)), or over HTTP from an active session. `TALE_ORIGIN` is your Tale origin and `TALE_SESSION_COOKIE` that session’s cookie header; `TALE_ORG_ID` and `TALE_WORKER_USER_ID` are the `organization.id` and `user.id` that `GET /api/v1/me` returns for the worker’s key:
```bash
GRANT_BODY=$(jq -n --arg user "$TALE_WORKER_USER_ID" \
'{userId:$user,competence:"tale:notifications.export",evidence:"Notification mirror worker"}')
curl -sS --compressed -X POST "$TALE_ORIGIN/api/app/governance/competences?orgId=$TALE_ORG_ID" \
-H "Cookie: $TALE_SESSION_COOKIE" \
-H "Origin: $TALE_ORIGIN" \
-H "Content-Type: application/json" \
-d "$GRANT_BODY"
```
The response is **201** with `{ "recordId": "…" }`. Add `expiresAt`, a future instant in whole epoch milliseconds no later than `8640000000000000`, to end the grant on its own; without it, the grant does not expire. A past instant returns **400** `COMPETENCE_EXPIRY_IN_PAST`, and a value that is not a whole number in that range **400** `invalid body`. While a grant is live, granting it again returns **409** `COMPETENCE_ALREADY_GRANTED`. Any other name under `tale:` returns **400** `COMPETENCE_CAPABILITY_UNKNOWN`, a user outside the organization **400** `COMPETENCE_USER_NOT_MEMBER`, and a session without the Owner or Admin role **403** `COMPETENCE_FORBIDDEN`. Before the first page, confirm with the worker’s key that `GET /api/v1/me` reports `capabilities.notificationExport: true`.
To withdraw the right, find the grant’s `id` in `GET /api/app/governance/competences?orgId=&userId=` with the same session, then send `POST /api/app/governance/competences//revoke?orgId=`. The worker’s next export request returns `403 ROLE_FORBIDDEN`. A revoked grant stays in the list as the audit trail; grant the capability again to restore the export.
### Select the recipient and stream
Set `TALE_RECIPIENT_EMAIL` to the intended member’s verified email address. The recipient must have exactly one matching, active membership in the selected organization. The endpoint checks membership and verification again on every page. Organization notifications use the recipient’s role for visibility: an administrator caller cannot export security notifications to a recipient who could not see them in Tale.
| Query parameter | Rule |
| --- | --- |
| `recipientEmail` | Required valid email, at most 320 characters; matching ignores case. |
| `stream` | Required: `personal` for the member’s personal feed, or `organization` for organization notifications visible to them. Read both separately for a complete mirror. |
| `locale` | Optional `en`, `de` or `fr`. Defaults to the organization’s language, with English message fallback. |
| `limit` | Default 100, maximum 100, minimum 1. Whole numbers outside the range are clamped. |
| `cursor` | Omit on the first request; pass the preceding `continueCursor` unchanged for the next page. There is no numeric `offset` parameter. |
```bash
curl --fail-with-body --silent --show-error --compressed --get \
"$TALE_URL/api/v1/notifications/sync" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG" \
--data-urlencode "recipientEmail=$TALE_RECIPIENT_EMAIL" \
--data-urlencode 'stream=personal' \
--data-urlencode 'locale=en' \
--data-urlencode 'limit=100'
```
A successful request returns `200` with `{recipientId, page, isDone, continueCursor}`. Missing, disabled, unverified or ambiguous recipients return `recipientId: null`, an empty `page`, `isDone: true` and an empty cursor. This is not an invitation or account-creation operation.
### Read rows and complete a scan
| Row field | Meaning |
| --- | --- |
| `id` | Stable source ID in the form `::`. Use it for matching and upserts within that recipient’s mirror. Organization notification IDs can be shared by recipients; keep their mirrors separate. |
| `version` | A 64-character hexadecimal SHA-256 hash of the exported row before the hash is added. Content or read-state changes change it; localized title/body changes can also change it. This is a change marker, not an incrementing sequence. |
| `title`, `body` | Text rendered from Tale’s notification catalogs and parameters, limited to 500 and 8,000 UTF-16 code units respectively. Keep the chosen locale stable across scans. |
| `path` | The same organization-scoped `/dashboard/...` destination used by the notification bell, including encoded identifiers and query parameters. Resolve it against the Tale browser origin, not an internal API transport URL. The user still needs access and any required private network connection. |
| `createdAt` | Creation time in epoch milliseconds. |
| `read` | Whether the intended recipient has read the notification in Tale. |
Personal pages are ordered by descending notification sequence; organization pages by descending creation time and ID. Both expose signed keyset cursors. A cursor is scoped to the organization, recipient and stream; never reuse it for another recipient or swap streams with it.
To keep the mirror consistent:
1. Start each stream without a cursor and upsert rows by `id`, comparing `version` for changes.
2. While `isDone` is `false`, send the returned cursor. At `true`, stop; do not send the empty final cursor.
3. Finish both streams successfully before retracting destination rows absent from this scan. If any page fails, preserve the previous mirror and recover the failed scan.
4. Begin later scans from the first page to notice read-state or text changes in older notifications. A cursor is a pagination position, not a change-feed checkpoint.
Reading the export never marks a Tale notification read and never deletes it. The endpoint provides no acknowledgement or write-back operation. Changing the destination’s read state does not change Tale’s.
### Recover an export request
| Response | Action |
| --- | --- |
| `401 UNAUTHORIZED` | Replace the missing, invalid or expired API key. |
| `403 ROLE_FORBIDDEN` | Use an Owner or Admin key in the selected organization, or have an Admin grant the key’s user `tale:notifications.export`; `capabilities.notificationExport` in `GET /api/v1/me` confirms it. An expired or revoked grant no longer permits the export. Recipient membership does not grant the caller export permission. |
| `400 INVALID_QUERY` | Correct missing or invalid recipient/stream/locale fields, unknown or repeated parameters, or blank cursor/limit. Inspect `data.issues`. |
| `400 INVALID_LIMIT` | Supply an integer limit. |
| `400 INVALID_CURSOR` | Restart the affected stream without a cursor. A removed or changed recipient can invalidate a cursor because membership is checked again. |
| `200`, `recipientId: null` | Check the recipient’s current membership and verified email. An empty completed page is not proof that an account exists. |
| `429 RATE_LIMITED` | Respect `Retry-After` and the [shared API budget](https://docs.tale.dev/develop/rate-limits); retain the previous mirror while waiting. |
The normal organization-selection errors also apply. Never replace a failed export with an empty successful result in the destination.
## Manage a project's agents
Every agent belongs to a project. The project ID is required in the URL for every operation; responses include both `projectId` and the agent's `id`. These are the same agents managed in the project's **Agents** tab, with the same access rules.
| Operation | Route | Success |
| ----------------------- | ----------------------------------------------- | -------------- |
| List the roster | `GET /api/v1/projects/{id}/agents` | `200 {agents}` |
| Create | `POST /api/v1/projects/{id}/agents` | `201 {agent}` |
| Read | `GET /api/v1/projects/{id}/agents/{agentId}` | `200 {agent}` |
| Save full configuration | `PUT /api/v1/projects/{id}/agents/{agentId}` | `200 {agent}` |
| Delete | `DELETE /api/v1/projects/{id}/agents/{agentId}` | `204` |
Choose an existing project, a harness `GET /api/v1/models` lists under `harnesses` — the ones the platform runs with its own credentials — and a model available to it. This example creates a Claude Code agent and reads back its configuration; it does not start a task.
```bash
: "${BASE:?Set BASE to your Tale origin}"
: "${TALE_API_KEY:?Set TALE_API_KEY}"
: "${ORG_SLUG:?Set ORG_SLUG}"
: "${PROJECT_ID:?Set PROJECT_ID to an existing project ID}"
: "${MODEL_ID:?Set MODEL_ID to a model served by your harness}"
AGENT_URL="$BASE/api/v1/projects/$PROJECT_ID/agents"
AGENT_BODY=$(jq -n --arg model "$MODEL_ID" \
'{name:"Reviewer",harness:"claude-code",model:$model,skills:[],connectors:[]}')
AGENT_ID=$(curl -fsS "$AGENT_URL" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $ORG_SLUG" \
-H 'Content-Type: application/json' -d "$AGENT_BODY" | jq -er '.agent.id')
curl -fsS "$AGENT_URL/$AGENT_ID" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $ORG_SLUG" \
| jq '.agent | {name, harness, skills, connectors}'
```
```json
{
"name": "Reviewer",
"harness": "claude-code",
"skills": [],
"connectors": []
}
```
### Save a full agent configuration
`POST` and `PUT` require `name`, `harness`, `model`, `skills` and `connectors`; a `harness` outside the eligible set returns **400**, `PROJECT_AGENT_HARNESS_INVALID`, with the set in `data.harnesses`. Optional fields are `modelProvider`, `tools`, `secrets` and `instructions`. A `PUT` saves the full configuration: omitted provider/instructions reset to `null`, and omitted tools/secrets reset to empty lists. It updates an existing agent; it does not create one at an unknown ID. Pass the `updatedAt` you last read as `expectedUpdatedAt` to make the save conditional: an agent that changed since returns **409**, `PROJECT_AGENT_STALE`, with the current `updatedAt` in `data`, and nothing is written — reload it and merge before saving again.
An agent whose `managed` is `true` is the organization's [standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent): Tale set it up in a project without agents of its own and keeps its runtime, model and instructions in line with the organization's settings. A `PUT` of it returns **409**, `PROJECT_AGENT_MANAGED`, and writes nothing; `DELETE` removes it like any other agent (contract 3.9.0).
### Validate models, grants, and limits
A project holds at most 50 agents. Names are unique within the project without regard to case, up to 120 characters; each equipment list allows 25 entries and instructions allow 20,000 characters. An invalid configuration or an exceeded limit returns **400**; a name another agent of the project already carries returns **409**, `PROJECT_AGENT_NAME_TAKEN` — the class every other duplicate on this endpoint returns, so reuse the existing agent rather than retrying.
`model` must be a model the organization's catalog lists (name `modelProvider` when several providers serve it) and `tools` must name known tool grants — a wrong value returns **400** with `PROJECT_AGENT_MODEL_INVALID`, `PROJECT_AGENT_PROVIDER_UNKNOWN` or `PROJECT_AGENT_TOOL_UNKNOWN` naming what to fix, instead of an agent that fails at its first task. `task_start_agent` lets the agent put another agent of the same project to work on a task (contract 3.8.0). `secrets` contains organization secret names, never values; a name the organization has not stored is refused with **400**, `PROJECT_AGENT_SECRET_UNKNOWN`, naming it in `data.secrets` (the app's dialog prunes such names; the API does not, so a typo never yields an agent that runs without its credential).
`task_update_metadata` is an optional project-agent grant (contract 3.10.0) for changing an existing task’s priority and agent assignment without execution. Its request names `taskId`, `priority` and/or `agentId`, plus the current value of each touched field in `expected`; `null` clears a value and omitted fields stay untouched. Build `expected.assignee` as `{type, id}` from `task_get`’s `assigneeType` and `assigneeId`, or use `null` when unassigned; an absent priority is `null`. A stale value refuses the whole request. The [project-agent guide](https://docs.tale.dev/platform/projects/project-agents) explains the protected review and live-run states. `task_upsert_by_external_ref` still uses `priority` only when creating a task.
`task_review` adds an optional project-agent grant in contract 3.11.0. It decides only a pending native review assigned to the live reviewer agent, for a completed run by a different agent in the same project. Pass `taskId`, `expected: {approvalId, runId, evidenceRevision}` from the current `task_get.pendingReview`, `decision: "approve" | "request_changes"`, nonempty `feedback`, and `evidence: {checks, pullRequests}`. Each check names its outcome (`passed` or `failed`) and details; a supplied GitHub pull request names its URL, exact `headSha`, and check state. Approval requires every supplied check to have passed. GitHub evidence is the agent's attestation, not a remote verification by Tale.
The tool rechecks the current grant, live issuer authority, recorded reviewer, implementation run, and local evidence revision. A stale result requires another read; changing the current assignee does not establish independence. Approval moves the task to `done`; `request_changes` moves it to `todo` with feedback and no execution, even if the feedback contains a mention. Human competence requirements and workflow approval gates remain outside this tool. There is no public REST endpoint for an agent verdict.
Since contract 3.15.0, native `task_get.agentRuns` includes an explicit `retryPending` boolean. Only the newest failed run of an existing agent can have `true`, using the same armed-retry, retirement and retry-budget checks as the task card. Preserve that retry. `false` does not authorize a restart or supply a provider reset time; an absent field on an older platform is unknown. Reconcile the current task, assignee, runs and review before acting, and honor provider waits and admission `retryAfter`. This adds no public REST endpoint or raw error text.
Contract 3.13.0 adds guarded repair admission to the existing native `task_start_agent` grant: pass `{taskId, resumeFrom: {kind: "review_repair", approvalId, runId}, feedback?}` for a recorded native `request_changes` decision. The server derives the implementation agent, feedback and feedback comment ID. It atomically requires the same latest settled source, unchanged implementation assignment, `todo`, no newer review or intervening status/assignment/archive decision, and the existing admission limits. An explicit `agentId` must match that source; `moveToInProgress: false` is refused. Untagged `{approvalId, runId}` retains its separate pending-question meaning. `stale_repair` starts nothing; do not fall back to an unguarded start.
A successful repair returns `started`, `runId` and `repairReceipt` with `taskId`, `approvalId`, `sourceRunId`, `feedbackCommentId`, `implementationAgentId`, admitted `runId`, `managerAgentId`, original `issuerRunId` and `admittedAt`. The same manager agent may recover it from a later live authorized run with `replayed: true`; every call rechecks current delegation and project authority, and replay has no effects. `task_get.reviewDecision` exposes one latest validated native verdict receipt, or `null` behind a newer pending, human or workflow review or unavailable metadata. It is historical evidence, not current task state; `null` does not prove a verdict was uncommitted. These additions create no public REST repair endpoint and do not make a verdict dispatch work.
Contract 3.12.0 also extends the native task tools. `task_find` accepts `reviewerAgentId` and returns `pendingReview: null` or `{approvalId, runId, reviewer}` per task. The filter matches the captured pending agent reviewer and is part of the signed cursor’s query identity; keep it unchanged when paging. It does not filter by the implementation assignee.
For a captured agent review with a usable source, `task_get.reviewFiles` returns `{expected, files, page, maxStageBytes}`. It lists up to 50 attachments and outputs; pass `page.continueCursor` back as `reviewFileCursor` while `page.isDone` is false. The cursor is bound to the task, reviewer, approval, source run and evidence revision. Entries carry their stored metadata and `unavailableReason`, or an explicit invalid-metadata reason when no safe file identity exists. An absent stored `runId` is not inferred.
The same `task_review` grant accepts `{operation: "stage_file", taskId, expected: {approvalId, runId, evidenceRevision}, fileId}` to stage one listed, available file of at most 20 MiB. It accepts no caller path, storage reference or URL. The response names the server-selected local `path` and `bytes`; the agent must read that file before attesting to its contents. Current task membership, captured review, live issuer, grant and policy are checked before transfer and again before success. Protected document bindings keep their document access rules; unsupported or unavailable files are refused. A concurrent revocation or handoff can leave previously authorized bytes in the workspace while refusing success and any stale verdict. This is a native tool operation, not a new public REST file or verdict endpoint.
Only organization Owners and Admins may change secret grants, so an editor's full save must preserve existing grants.
Project readers can read the roster; writes require project edit access and an active project. An invisible or missing project, or an agent ID from another project, returns **404**. A multi-organization key must include `X-Organization-Slug` on reads and writes. [Project agents](https://docs.tale.dev/platform/projects/project-agents) explains how these agents work on tasks; direct chat keeps using the built-in assistant.
## Automation names in URLs
An automation's name is a `/`-separated path — `billing/dunning` — and a path cannot travel inside one URL segment. In every `.../automations/{name}/...` URL, write the name with `__` in place of each `/`:
```bash
curl -sS --compressed "https://your-host.example.com/api/v1/automations/billing__dunning/versions" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $TALE_ORG_SLUG"
```
Responses always carry the real name (`"name": "billing/dunning"`); the `__` form exists only in URLs. Skill slugs are flat and need no encoding. Project agents use their project ID and agent ID.
### Read the version that will run
`GET /api/v1/automations` lists each automation with its `latestVersion`, `deployedVersion` and `projectIds` — the projects it is installed in, which the run routes below require — plus what a launcher needs without a second call: its `description`, the `inputs` schema a run must match (the deployed version's, else the newest saved one's) and its `trigger` — kind, switch and health: `lastFiredAt`, `lastSkippedAt` and `lastSkipReason`, the same stamps `GET .../triggers` reads, so one listing call finds every binding that is enabled and not firing — or `null` when none is bound.
`GET /api/v1/automations/{name}` returns the newest saved version by default (`?version=latest` spells the default out), which may be a draft; a live run executes the deployed one, so read the contract of the code that actually runs with `?version=deployed` (a number names any saved version). A version the automation does not have returns **404** `AUTOMATION_VERSION_UNKNOWN` — `?version=deployed` while nothing is deployed too — where an unknown automation returns `AUTOMATION_NOT_FOUND`.
`GET /api/v1/automations/{name}/versions` names the `deployedVersion` and marks each row `deployed`, and each row carries the version's test verdict: `testsPassed` is `null` until the tests were run (a document without tests stays `null`), else `true` or `false` for the last run — the save's, when the MCP `save_automation` saves a document with tests, or the deploy gate's, which persists a refusal — with `testsCheckedAt` saying when — `null` beside a verdict recorded before 0.5.24 kept the time, so branch on `testsPassed` for the verdict and on `testsCheckedAt` for its freshness only; the latest verdict wins.
`DELETE /api/v1/automations/{name}` removes the automation, its versions, triggers and project bindings included — its runs stay: they remain listed by `GET /api/v1/runs`, readable by id, and still answered by name at `GET /api/v1/automations/{name}/runs` (and its project twin) under the name they ran as, which `GET /api/v1/automations/{name}`, its versions and its triggers then return **404** for — and returns **409** `AUTOMATION_HAS_ACTIVE_RUNS` while a run is in flight; it needs the developer capability. Creating, saving and deploying an automation is not on this surface: that is the [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint)'s `save_automation` and `deploy_automation`, the app's canvas, or a `tale deploy` configuration release — REST lists, reads, runs, installs and wires triggers for automations built there.
## Triggers
A trigger starts an automation without a call from you: on a schedule, from a webhook URL, or when the platform raises an event. Bind one with `PUT /api/v1/automations/{name}/triggers` — one trigger per automation, and the `PUT` replaces whatever was bound:
```bash
curl -sS --compressed -X PUT "https://your-host.example.com/api/v1/automations/billing__dunning/triggers" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "kind": "event", "event": "contact.created" }'
# → 200 { "name": "billing/dunning", "deployed": true }
```
### Choose the trigger kind
`kind` is `schedule` (with a five-field `cron` and an optional IANA `timezone`), `webhook` (the response carries the URL's `token` once — the [Webhooks page](https://docs.tale.dev/develop/webhooks) covers that endpoint) or `event`. A trigger that could never fire is refused with **400** `AUTOMATION_TRIGGER_INVALID` and a sentence naming the fix: a cron that matches nothing (including a day no named month has, `0 0 30 2 *`), a time zone that is not an IANA zone, an event the platform does not raise.
Each kind takes its own keys — `cron` and `timezone` only with `schedule`, `event` only with `event`, `rotateToken` only with `webhook` — and a key that belongs to another kind is refused as an unknown key (**400** `INVALID_BODY`, naming it under `data.issues`), so a webhook trigger can never read back as one that also runs on a schedule. An event trigger binds one of the events the platform raises today, and the run's input is `{ "trigger": "event", "event": "", "payload": }`:
| Event | Raised when |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `contact.created`, `contact.updated`, `contact.deleted` | a contact is created, changed or deleted — through the API, the app or an import |
| `conversation.created` | a conversation opens in Inbox — an email arriving, or an external conversation mirrored in |
| `conversation.message_received` | a message lands on an existing conversation |
| `project.created` | a project is created |
| `task.created` | a task is created — on a board, through the API, or by an intake |
| `task.status_changed` | a person moves a task to another status (an agent's own moves raise nothing, so an automation cannot re-trigger itself) |
| `comment.created` | a comment lands on a task |
| `comment.mentioned` | a task comment mentions someone with `@` |
### Check trigger health and pause safely
`GET .../triggers` reads the binding back — as `triggers`, a list of at most one, the one plural in the family — with its health: `lastFiredAt` is the last time this binding **started a run** — `lastRunId` names it, and both stay `null` until it has — while `lastSkippedAt` and `lastSkipReason` record the last time it came due and started nothing: `not_deployed` (nothing is deployed — deploy a version), `unusable_cron` (the expression or zone could not be read; the scheduler leaves the binding alone until it is edited), `start_refused` (the deployed version's `inputs` schema refused the run's input) or `paused_after_failures` (a schedule that turned itself off after repeated failures — see below). A webhook delivery the deployed `inputs` schema refuses is a different case: it is answered **400** `AUTOMATION_INPUT_INVALID` to the sender and starts nothing, and it moves none of these stamps — the binding did not come due, so a webhook whose every delivery is refused reads the same as one that has never been called. Verify deliveries from the sender's side.
A binding is alive when `lastFiredAt` keeps pace with its cadence; one whose `lastSkippedAt` is the newer stamp is coming due and not running, and the reason says what to fix. A rebind to another kind starts every stamp afresh. `enabled: false` pauses a trigger without losing it; `DELETE .../triggers` removes it — and, for a webhook, revokes the URL. So does binding another kind over a live webhook: the `PUT` still returns **200**, with `"revoked": "webhook"` beside the name, and the old URL is gone for good — a later webhook bind mints a different token.
A schedule whose runs keep failing pauses itself. `consecutiveFailures` counts the runs this binding started that failed in a row with a `failureCode` the next occurrence would repeat — `node_error`, `connector_error`, `llm_output_invalid`, `auth_error`, `missing_api_key`, `credit_exhausted` or `model_not_found` — and `lastFailedAt`, `lastFailureCode` and `lastFailedRunId` name the last of them. A success sets the count back to `0`, and any other failure neither counts nor resets it. A schedule that has paused itself (`enabled: false` with `lastSkipReason: "paused_after_failures"`) is the exception: it keeps the count that paused it, even when a run still in flight at the pause succeeds afterwards, and only a `PUT` clears it. When a schedule's count reaches five, the platform sets `enabled: false` and `lastSkipReason: "paused_after_failures"`, writes an `automation.trigger.paused` audit row, and notifies the organization's Owners and Admins. Fix the automation, then `PUT` the trigger with `enabled: true`. Every `PUT` resets the count and clears that reason, and one that omits `enabled` turns the trigger back on, since `enabled` defaults to `true`. Webhook and event bindings keep the count but are never paused (contract 3.1.0).
The `PUT` also returns `deployed`: binding before deploying is accepted, and a trigger bound to an automation with no deployed version starts nothing — every occurrence is skipped as `not_deployed`, which the row's `trigger` on `GET /api/v1/automations` shows — until a version is deployed.
## Start a run, then poll it
A run is durable and may take minutes, so starting one returns **202** with the run's identity, not its result:
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//automations/billing__dunning/runs" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "input": { "customerId": "cus_123" } }'
# → 202 { "runId": "...", "version": 2, "name": "billing/dunning", "mode": "live" }
```
### Interpret running and waiting states
Poll `GET /api/v1/projects/{id}/runs/{runId}?fields=status,finishedAt` until `status` leaves `queued`/`running`/`waiting` — naming the keys keeps the poll to a line instead of the whole run, and sending the answer's `ETag` back as `If-None-Match` turns an unchanged poll into a bodiless **304** ([caching](https://docs.tale.dev/develop/api-reference#caching-compression-and-partial-reads)); then read the run in full: it carries `output`, the per-node `trace`, and the `effects` it produced.
`waiting` covers two families and only one needs you: while a run is parked, `waitingFor` says on what — `approval` (a person's decision on a gate) and `ask` (a question a person has to answer) need a human; `agent` (an agent turn still running), `room` (an agent turn whose start waits for sandbox room, since contract 3.14.0) and `repeat` (a node polling until its `repeatUntil` condition holds) do not, and a run can sit in any of them for minutes while healthy. "Runs that need a person" is `waitingFor` in (`approval`, `ask`) — never `status=waiting` alone, which fills with polling runs. `detail` names the park (`approval:`, `agent:`, `room:`, `repeat:`) and, once failed, the failure sentence.
Failed runs expose a stable `failureCode` alongside the human-readable `detail`. Non-failed runs return `null`; older failed records can also have no code. Run summaries omit an unset code.
A run a trigger started (`startedBy: "trigger:"`) also carries `startedVia` — `schedule`, `webhook` or `event` — read off the run's own input, so a listing tells a scheduled run from a webhook delivery, and an old run keeps its kind after the binding changes. Runs a person or an API key started omit it (contract 2.1.0).
| Failure family | Examples and next action |
| --- | --- |
| Automation engine | `node_error`, `connector_error`, `llm_output_invalid`, `approval_rejected`, `execution_limit`, `automation_deleted`: inspect the failed node and its trace. Correct the input or definition; if a person rejected an operation, address their reason before requesting another run. |
| Model provider | Codes such as `credit_exhausted` or `rate_limited`: resolve the provider condition before another attempt. |
| Agent execution | Codes such as `harness_error`, `session_gone`, `deadline`, or `budget_exceeded`: inspect the agent’s detail and limits. The complete enum is in OpenAPI. |
A failure code identifies the cause; it does not make a whole-run retry safe. Earlier nodes may already have changed external systems. `startedAt` records when the start was accepted, before a worker claims it. There is no separate pickup timestamp, so `finishedAt - startedAt` includes queueing and other waits.
`POST /api/v1/projects/{id}/runs/{runId}/cancel` stops a run at its next node boundary; completed work is not undone. The response includes `cancelled` and the resulting `status`. A successful cancellation returns `cancelled: true`, `status: "cancelled"`, and clears the run’s `detail`. If the run already finished, `cancelled: false` accompanies its terminal status (`success`, `failed`, or `cancelled`).
### Retry a start without creating another run
A start is safe to retry when you name it: send `Idempotency-Key: ` and a repeat within 24 hours — a retried timeout, a lost response — returns **202** with the run the first attempt started and `"duplicate": true`, so no second run exists; the same key with a different body returns **409** `IDEMPOTENCY_KEY_REUSED`. The key is scoped to the automation and the URL project, and a refused start remembers nothing, so the same key runs once the refusal is fixed.
An optional `Idempotency-Key` must contain 1–255 printable ASCII characters after surrounding whitespace is removed. A header that is present but blank, too long, or contains other characters returns `400 INVALID_HEADER` with the header named in `data.issues`; nothing starts. Reuse the same trimmed value and body for a retry. Omit the header only when you do not want replay protection.
### Choose live or mock execution
`mode` defaults to `live`; arbitrary live runs and run cancellation require `capabilities.developer` from `/me`. Project runs also require edit access to an active project, including `mode: "mock"`. Mock runs use deterministic mocks; a non-project mock run needs only membership. Starting a run needs no trigger. An automation with no deployed version returns **409** unless a saved version is explicitly selected for a mock run.
An unknown automation returns **404**. A live run can only use the deployed `version`; naming another saved version returns **409**. Use `mode: "mock"` to test another saved version. A missing body means `{}`, but malformed JSON returns **400** and starts nothing. When the automation declares an `inputs` schema, the input must match it before a run is created: a mismatch returns **400** `AUTOMATION_INPUT_INVALID` with every problem under `data.issues` (`path`, `message`), the way a refused body does. `input` defaults to `{}` only when it is absent — `null` is sent as null, for the schema to judge.
### Select scope and browse run history
The project in the URL is the context for the run's task and document tools. An automation with project bindings can run only in a bound project; one with no bindings runs in any project the caller can edit — installing it (`POST /api/v1/projects/{id}/automations/{name}`) lists it under that project and scopes it, it is not a gate a never-bound automation has to pass. `GET /api/v1/projects/{id}/automations/{name}/runs` lists that project's history for one automation, `GET /api/v1/projects/{id}/runs` for every automation.
Listings answer summaries — identity, scope, status and timing, each row naming the run as `id` and, under the name the start answered, `runId`, one value under both names — newest first as `{ "runs": [...], "isDone": ..., "continueCursor": ... }`: add `?status=failed` (one or more statuses, comma-separated) to narrow them, `?include=input,output` (also `trace`, `effects`, `checkpoints`) to inline the full-row fields a summary leaves out — an inlining page reads at most 25 rows, is bounded at 8 MiB of them, and ends early, `isDone: false`, when the next row would not fit — and pass `continueCursor` back as `?cursor=` until `isDone`.
`GET /api/v1/runs` is the cross-cutting view: every run the key holder can see, organization runs and the runs of visible projects alike, each row naming its `projectId`. For an automation with no bindings, `POST /api/v1/automations/{name}/runs` starts a non-project run; a bound automation returns **409** there. `GET /api/v1/automations/{name}/runs` and `/api/v1/runs/{runId}` expose only non-project runs. A project run requires its project URL for reading, cancellation and deletion. `DELETE /api/v1/projects/{id}/runs/{runId}` (or `/api/v1/runs/{runId}`) removes a finished run — stored input and output included — under the developer capability; a run still in flight returns **409** `RUN_ACTIVE`, so cancel it first.
## Act for a member: answer a run’s question, decide a task’s review
A run parked on `waitingFor: "ask"` waits on a person; a task in `in_review` can wait on a person or a designated agent. These two REST doors relay a person's gesture from another application and name that person as `actor`, so Tale records the person rather than the key. Both doors need API contract 1.16.0; contract 3.11.0 adds the explicit refusal of human approval while a task review belongs to an agent.
### Answer the question a run is waiting on
`GET /api/v1/projects/{id}/runs/{runId}/ask` answers the live question as `PendingAsk` — the sentence, an optional structured `questions` set, the node that asked and the `expiresAt` deadline — or `ask: null` when nothing waits on a person. Reading it takes the same access as reading the run.
```bash
curl -sS --compressed "https://your-host.example.com/api/v1/projects//runs//ask" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "ask": { "askId": "...", "question": "...", "expiresAt": 1758210000000, "taskId": "..." } }
```
Send the answer to `POST /api/v1/projects/{id}/runs/{runId}/asks/{askId}`. Tale records it, resumes the run in the same transaction, and puts the answer on the task timeline as the answerer’s own comment. For a `questions` set, send one line per question the way the app does: ` → ; (in their own words)`.
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//runs//asks/" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "answer": "Book it in February.", "actor": { "email": "reviewer@example.com" } }'
# → 200 { "ok": true, "askId": "...", "runId": "...", "answeredBy": "", "actorUserId": "", "taskId": "..." }
```
A project run needs write access to an active project; an organization run needs membership. Without `actor`, the key answers as itself and `answeredBy` reads `api-key:`. A question that was already answered or closed returns **409** `HUMAN_ASK_NOT_PENDING`, one past its deadline **409** `HUMAN_ASK_EXPIRED` — the run then fails with `failureCode: "ask_expired"` — and a question this run did not ask **404** `HUMAN_ASK_NOT_FOUND`. A blank answer returns **400** `EMPTY_ANSWER`.
### Decide a task’s review
`GET /api/v1/projects/{id}/tasks/{taskId}/review` answers the task's status and its pending `TaskReview`, or `review: null`. Since contract 3.11.0, `reviewer` is `{kind: "user", userId}`, `{kind: "agent", agentId}`, or `null`; `implementationAgentId` identifies the actual source agent and `evidenceRevision` identifies the current local review evidence, both nullable. `requestedFor` remains the human compatibility field and is `null` for an agent; `agentSlug` is display text, never source identity. A `POST` on this path relays a human decision and requires `actor`:
Contract 3.12.0 adds `agentReviewBlockedReason`: `null`, `reviewer_unavailable`, `permission_missing`, `source_required`, `source_changed`, `self_review`, `human_policy`, or `policy_unavailable`. The same diagnosis appears in native `task_get.pendingReview`. It is derived from current authority and source, never a new review state or an automatic transfer. Source-less submissions and new reviews under a human independence or competence policy use the human chain; already captured agent reviews require explicit handoff.
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//tasks//review" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "decision": "approve", "actor": { "email": "reviewer@example.com", "userId": "" } }'
# → 200 { "task": { "id": "...", "status": "done" }, "decision": "approve", "approvalId": "...", "actorUserId": "" }
```
`approve` is the board’s move to Done: the member’s own right to change the task (an Editor or higher, or the member who created it or is its person assignee) and the organization’s `review_policy` apply exactly as there (**403** `REVIEW_INDEPENDENT_REVIEWER_REQUIRED` or `REVIEW_COMPETENCE_REQUIRED` when the policy refuses them), the review is recorded as approved by the member, and the task becomes `done`; a task with open subtasks returns **409** `TASK_HAS_OPEN_SUBTASKS`. `request_changes` needs `comment` and `workflowSlug`: it withdraws the review, puts the comment on the timeline and starts the workflow again on the task, which reads the comment as feedback; the answer carries the `runId` to poll, with `started: false` when a live run was reused. A task that is not in review returns **409** `TASK_NOT_IN_REVIEW`. Every decision is audited as `task.review_relayed`, naming the member and the key that relayed for them.
Human `approve` also reads the current policy strictly. Invalid or unreadable `review_policy` configuration returns **409** `TASK_REVIEW_POLICY_UNAVAILABLE`, even after an explicit agent-to-person transfer; no approval or completion is recorded. Restore valid configuration and read the review again. A genuinely absent optional policy remains supported. This refusal does not add a policy-approval gate to `request_changes` or withdrawal.
Approving a review assigned to an agent returns **409**, `TASK_AGENT_REVIEW_REQUIRED`. An Editor must explicitly [transfer the pending review](https://docs.tale.dev/platform/projects/tasks#transfer-review) to an eligible person before this human approval endpoint can accept it. Do not impersonate the agent in `actor` or treat a missing human `requestedFor` as an unowned review.
### Name the member the gesture is for
`actor.email` names the member by e-mail. Tale resolves it against the organization with the same rule as the notification export: exactly one active membership whose address is verified. No such member returns **404** `ACTOR_NOT_FOUND`, two **409** `ACTOR_AMBIGUOUS`, an unverified address **403** `ACTOR_UNVERIFIED`, a disabled membership **403** `ACTOR_DISABLED`. Every answer returns the resolved `actorUserId`; pin it as `actor.userId` on later calls, and an address that has since moved to another account returns **409** `ACTOR_REBOUND` instead of acting as its new holder. A member who may not see the project — or, on the review door, not change its task — returns **403** `ACTOR_FORBIDDEN`; the key holder's own access is checked first, so this code always speaks of the actor.
Naming an actor is a right of its own. An Owner or Admin key has it by role; any other key holder needs the `tale:rest.act-as` capability, granted and revoked exactly like the export capability in [Delegate the export without an Admin role](https://docs.tale.dev/develop/api-reference#delegate-the-export-without-an-admin-role), with `"competence":"tale:rest.act-as"` in the grant body. `GET /api/v1/me` answers it as `capabilities.actAs`; an `actor` sent without it returns **403** `ROLE_FORBIDDEN` before any member is looked up. The member’s own permissions still decide what the relayed gesture may do.
## Send a message, then poll the turn
Project chat follows the same 202-then-poll shape. Use a project you can read, create a thread, post a message, poll the generation, then read the messages:
### Choose a callable model
List models before sending a message. Each entry carries what a client needs to choose — `contextWindow`, `maxOutputTokens`, `capabilities` (`tools`, `vision`, `reasoning`), `pricing` when the catalog publishes one, `tags` — and `default: true` marks the organization’s pick for this key holder; it appears only when the organization pins a default model, so do not wait for it. Use an entry’s `id` as `model`; add its `providerSlug` when the same id is listed under more than one provider.
`maxOutputTokens` is omitted when the catalog declares no ceiling; the send route then has no catalog ceiling to enforce. Do not require this field when reading the model list.
The list respects the organization’s model-access policy and includes only models callable directly through REST; an empty list means no chat model is available to this key holder.
`capabilities` and `tags` describe the model, not what this surface can send it: the REST send is text only (`content`), so a `vision` model reads an image here only when the thread was continued from the app with an image attachment — a data URI pasted into `content` reaches the model as text and is answered as text, and nothing on the wire marks it; image input over REST is not offered in this version.
The list is the organization’s configured catalog, not a promise from the provider’s account: an operator excludes a model the provider’s plan does not cover through the credential’s model allowlist in Settings. The pair is checked on send, at the endpoint: an id the list does not carry returns **400**, `CHAT_MODEL_UNKNOWN`; an id several providers serve, with none named, **400**, `CHAT_MODEL_AMBIGUOUS` with the candidates in `data.providers`; a `providerSlug` the list does not carry **400**, `CHAT_PROVIDER_UNKNOWN`, and one that does not serve the chosen `model` **400**, `CHAT_MODEL_NOT_ON_PROVIDER`.
The 202 names the provider the turn runs on, and the turn never falls back to another provider behind your back.
```bash
curl -sS --compressed "https://your-host.example.com/api/v1/models" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# No available model → 200 { "models": [] }
```
```bash
# 1. A thread of your own
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//threads" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" -d '{}'
# → 201 { "id": "" }
# 2. Send a message — on this API the model is always explicit, never auto-selected
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//threads//messages" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "content": "Summarise this quarter for me.", "model": "", "providerSlug": "" }'
# → 202 { "threadId": "...", "status": "accepted", "model": "...", "providerSlug": "...", "messageId": "", "poll": "/api/v1/projects//threads//generation" }
# 3. Poll until idle, then read
curl -sS --compressed "https://your-host.example.com/api/v1/projects//threads//generation" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "status": "queued", "messageId": "..." } … then { "status": "streaming", "messageId": "...", "text": "The quarter…", "textOffset": 0, "textLength": 12, "reasoning": "", "reasoningOffset": 0, "reasoningLength": 0, "cancelRequested": false, "updatedAt": 1774... } … then { "status": "idle", "lastMessageId": "", "lastStatus": "complete" }
# 4. Read the reply by the id the 202 named
curl -sS "https://your-host.example.com/api/v1/projects//threads//messages/" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "id": "", "role": "assistant", "status": "complete", "finishReason": "stop", "parts": [ … ], "usage": { … }, … }
```
### Poll your accepted message
Keep the `messageId` returned by the send's `202`: this is the assistant message that will contain your result. Poll generation every two to five seconds and inspect its state:
Accepted chat messages share one oldest-first queue across the deployment’s organizations and keys. The background worker processes batches of up to `WORKER_CONCURRENCY` turns (default 5), and the next batch waits for the current batch to settle. Acceptance is not immediate execution; a quiet thread can wait behind work from other clients. The API does not expose queue position or an estimated start time.
| Generation state | Meaning and next action |
| --- | --- |
| `queued` | Accepted and waiting for a worker; keep polling. Until a worker opens the turn this poll is its only view: `GET .../messages` does not list the turn yet (the page reads complete without it) and `.../messages/{messageId}` can answer **404** for the id the send named |
| `streaming` | The provider is producing output; `text` and `reasoning` contain the current partial result |
| `idle`, matching `lastMessageId` | Your turn has settled; `lastStatus` describes its outcome |
| `idle`, different `lastMessageId` | The summary refers to another message; read your saved message ID directly before drawing a conclusion |
Read the result at `GET /api/v1/projects/{id}/threads/{threadId}/messages/{messageId}`. To browse the transcript newest first, use `GET .../messages?order=desc`; a cursor stays bound to the direction in which it was issued. A later turn can change the latest-message summary, so a different ID is not evidence that your earlier turn never ran.
Set a timeout for each poll request and a separate overall deadline for your client; 30 seconds per ordinary poll is a reasonable starting point. The server has no fixed total turn deadline. It abandons a provider request after 180 seconds without activity, and each received byte, including reasoning output, resets that timer. High reasoning effort can therefore keep a turn active before any answer text appears. If you no longer need the turn, call `DELETE .../generation` rather than merely stopping your polling loop.
For incremental updates, send the previous `textLength` as `since` and `reasoningLength` as `reasoningSince`. The unit is UTF-16 code units — JavaScript `String.length`, an emoji counting two; not code points — so echo the lengths the poll answered rather than counting characters yourself. The response's `textOffset` and `reasoningOffset` locate the returned slices: the value you sent, one lower when that would have split a surrogate pair (the slice then re-sends the whole character), or 0 when a completed tool round reset that stream. Reassemble by one rule, `held = held.slice(0, textOffset) + text`, and none of the three cases needs its own handling.
If the send response was lost, read `.../generation` before sending again. `queued` or `streaming` identifies the active turn. When idle, inspect the latest assistant message or find your newest user message by its `content`; without a saved message ID, do not assume an unrelated latest result belongs to the lost request.
### Retry a send safely
A send is safe to retry when you name it: send `Idempotency-Key: ` and a repeat within 24 hours — a retried timeout, a lost 202 — returns **202** with what the first attempt answered, the same `messageId` included, and `"duplicate": true`, so no second turn runs or bills; the same key with a different body returns **409**, `IDEMPOTENCY_KEY_REUSED`. The key is scoped to the thread and the URL project, and a refused send (a thread still mid-turn) remembers nothing, so the same key sends once the poll says idle.
An optional `Idempotency-Key` must contain 1–255 printable ASCII characters after surrounding whitespace is removed. A header that is present but blank, too long, or contains other characters returns `400 INVALID_HEADER` with the header named in `data.issues`; nothing starts. Reuse the same trimmed value and body for a retry. Omit the header only when you do not want replay protection.
### Understand assistant behavior and token limits
A turn is the complete handling of one accepted message, including model rounds and tool calls. REST chat uses the built-in workspace assistant with its instructions, safety rules, and three retrieval tools. These add roughly 3,000 prompt tokens per model round, included in `usage.inputTokens`. A tool-using turn can take up to five rounds, each billing its full prompt. Requests for deliverables such as documents or reports are directed to Tasks.
Project threads can retrieve that project's files and the organization's knowledge hub, but not another project's files. Threads without a project can retrieve the hub only. The assistant chooses whether to search based on the question; naming a file or explicitly asking it to search helps express your intent, but no request field forces a retrieval call.
| Field | Behavior |
| --- | --- |
| `reasoningEffort` | `low`, `medium`, `high`, `extra`, or `max`; ignored when the model lacks `capabilities.reasoning` |
| `maxOutputTokens` | Budget for the whole turn across model rounds; must not exceed the model's declared ceiling from `/models` |
An excessive output budget receives `400 INVALID_BODY` naming the ceiling. Each round receives only the remaining budget; a round with no budget left does not start. Reasoning tokens count toward this budget and `usage.outputTokens`, and are billed at the output-token price. They can consume the entire allowance, leaving a billed `complete` response with no answer text and `finishReason: "length"`. Increase the output allowance or lower reasoning effort when your task needs room for a visible answer.
Thinking-budget providers using Anthropic-style extended thinking have one exception: a budget below 2,048 is raised to 2,048, allowing at least 1,024 for thinking and as much for the answer. Effort-based reasoning models, including the GLM and DeepSeek families, have no such floor.
Read `finishReason`, not just `status: "complete"` or the token count. The values are `stop`, `length`, `tool-calls`, `content-filter`, `cancelled`, and `other`; it is absent when the provider supplies none. When a round ends at the length cap, every tool call from that round is withheld, even if one call’s arguments are complete. Each withheld `tool-result` has `status: "invalid_args"`; its message distinguishes complete from truncated arguments. Final text can still look complete, so inspect the stopping reason and tool results.
An empty answer, or cancellation before any text, has no text part. `parts` can be empty or contain only reasoning and tool parts. Check for actual text before showing or exporting an answer.
Threads, messages, and generation status are visible only to their owning key holder. Sharing project membership does not reveal another user's chats. Use `GET /api/v1/projects/{id}/threads` to list yours and `GET /api/v1/projects/{id}/threads/{threadId}` to read one.
### Read language, status, usage, and message parts
`content` is trimmed; a blank prompt — nothing but whitespace and invisible format characters such as zero-width spaces — receives `400` without starting a turn (markdown that renders as nothing, an empty code fence say, is still a prompt). Optional `locale` is a BCP 47 language tag such as `de` or `en-GB`. It asks the assistant to answer in that language through system and message instructions. Organization-mandated instructions take precedence. Without it, the assistant uses the prompt's language.
Language is an instruction to the model, not a validated output guarantee. A model can answer in another language, especially with a short prompt and reasoning enabled; the response has no language-mismatch flag. If a specific language is required, inspect the answer before accepting it.
| Message status | Meaning |
| --- | --- |
| `pending` | Assistant row exists with empty `parts` while the turn runs; generation names its `messageId` |
| `complete` | Turn finished; inspect `finishReason` for truncation or other stopping conditions |
| `cancelled` | Stopped, with any partial output already received |
| `failed` | Failed, with `error` and `errorCode` when available |
| Usage field | Interpretation |
| --- | --- |
| `inputTokens`, `outputTokens` | Reported input and output counts |
| `reasoningTokens` | Share of output spent reasoning; absent means unreported, while `0` is a reported zero |
| `cachedInputTokens` | Share of input served from the provider's cache |
| `costEstimateCents` | Catalog estimate in fractional US cents, rounded to a millionth of a cent; absent without catalog pricing |
| `estimated: true` | Platform-estimated counts, typically when provider counts were lost to a cancellation or a failure mid-stream |
| `stepLimitHit: true` | The tool loop used its full round budget |
The usage ledger books the same catalog estimate. Cached input is priced at the normal input rate, so the estimate is an upper bound for a cached turn. Estimated usage includes the whole prompt, including assistant tools. A failed turn still carries `usage` for what it consumed: its finished model rounds and, if the provider had already accepted the request, the round that failed, estimated where the provider reported no counts. A round the provider refused adds nothing, whether it answered an HTTP error status or reported a refusal such as a rate limit before any answer or positive reported usage, so a turn refused on its first round has no `usage`.
`parts` is an ordered list discriminated by `type`: `text`, `reasoning`, `attachment`, `tool-call`, `tool-result`, or `approval`. The OpenAPI schema types each variant as its own named schema (`TextPart`, `ReasoningPart`, `AttachmentPart`, `ToolCallPart`, `ToolResultPart`, `ApprovalPart`) behind a `type` discriminator with an explicit mapping, so a generated client gets a class per kind. Treat future unknown variants as opaque rather than failing the whole message.
A `reasoning` part is thinking, not the final answer. It may repeat instructions supplied to the model, including organization and project instructions and retrieval trust rules. Display it separately and only to an audience allowed to see those instructions.
### Respect thread scope and access
For a personal chat with no project, use `/api/v1/threads` and its corresponding detail, messages and generation paths. Those URLs cannot address project threads. A wrong project URL returns **404**. Both kinds use the built-in assistant; `projectId`, `agentSlug` and `agentId` in create or message bodies answer **400**. Project readers, including Members, may create and send; an archived project refuses these writes with **403**.
An archived thread refuses a message with **409**, `CHAT_THREAD_ARCHIVED`, a sandbox thread with **409**, `CHAT_THREAD_NOT_DIRECT`, and a thread whose turn is still running — or whose accepted send is still queued — with **409**, `CHAT_TURN_IN_PROGRESS`; nothing is queued and the running turn keeps its `messageId`. Retry the last one once the poll says idle, never the other two.
### Rename, archive, delete, or stop a thread
The lifecycle is yours through the same URLs. `PATCH .../threads/{threadId}` with `{ "archived": true }` archives a thread out of the way and `false` restores it (a thread whose turn is running, or whose send is still queued, refuses the archive with **409** `CHAT_TURN_IN_PROGRESS` as the delete does — cancel first; restoring and renaming stay open mid-turn), and `{ "title": "Q3 review" }` renames it (send at least one of the two; a title is trimmed and 1–120 characters, on create and on rename alike); a thread created without a title is named by the assistant after its first message, and the thread’s `title` carries the name either way.
Archiving stamps `archivedAt` on the thread and leaves `updatedAt` alone — `updatedAt` is the last message activity, so a sync that watches it must read `archived` and `archivedAt` to see an archive or a restore. `DELETE .../threads/{threadId}` moves it to the trash (**409**, `CHAT_TURN_IN_PROGRESS` while a turn runs or a send is still queued); `DELETE .../threads/{threadId}/generation` asks the running turn to stop — **202** `{ "status": "cancelling", "messageId": "..." }`, then poll until idle; the stopped reply settles as `status: "cancelled"` with whatever had streamed.
A send that is still queued (the poll says `queued`) is stopped the same way: **202** naming the reply the send's 202 promised, the model is never called, and that reply settles as `cancelled` with empty `parts`. **404**, `CHAT_TURN_NOT_RUNNING` when nothing runs and nothing is queued — its `data.lastMessageId` and `data.lastStatus` name the newest assistant message the way an idle poll does, so a stop that lost the race against a fast model reads as "the reply is already there" without a second call. An archived project refuses all three with **403**.
### Recover from model and access failures
A model failure can appear as an assistant message with readable `error` text and, when available, `errorCode`. The model list is the organization’s configured catalog, not a promise from the provider’s account, so two codes mean the account rather than the request: `credit_exhausted` (the balance is spent) and `model_not_entitled` (the provider’s plan excludes this model). `error` is the provider's own answer prefixed with its HTTP status — a provider **429** can classify as `model_not_entitled` when the plan, not the rate, refused the model — so branch on `errorCode`, never on the sentence.
Pick another model or fix the account — waiting changes nothing, and neither is a `rate_limited`. The worker rechecks the accepted thread and project access before opening the turn. If the thread moves projects or access is lost while the request waits, it does not run or append an error in the new scope.
## Compatible model endpoints {#model-endpoints}
Three routes answer the OpenAI and Anthropic interfaces for the organization's models, so an SDK or a coding tool that speaks either one can use Tale as its model provider (contract 3.7.0). They are a bare model door: the request goes to the model it names and the answer comes back as the model sent it, with no assistant, thread, or Tale tool in between. They are off until an Admin turns on **Model endpoints for API keys**. Owners, Admins, and Developers may then call them; any other member needs a live `tale:models.api` grant. [Use Tale from your editor or a script](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints) covers client setup.
| Route | Serves |
| --- | --- |
| `POST /api/v1/openai/chat/completions` | OpenAI Chat Completions, streamed as server-sent events or answered whole, with tools and `tool_calls`, images, and files. A stream ends with `data: [DONE]`; its closing usage chunk comes only when `stream_options.include_usage` asks for it. |
| `GET /api/v1/openai/models` | The model ids the key holder may call, as an OpenAI list: `owned_by` is the provider slug, and `created` is always `0`. |
| `POST /api/v1/anthropic/v1/messages` | Anthropic Messages, streamed or whole, with tools, images, and documents. `anthropic-version` and `anthropic-beta` are passed on. |
Authentication is the same as on every other operation: `Authorization: Bearer ` and, for a key holder with several memberships, `X-Organization-Slug`. The routes share the general rate budget and answer with `X-Request-Id` like any other. They ignore query parameters, an Anthropic SDK's `?beta=true` included, and take bodies up to 32 MiB. Tale reads the fields it governs and relays the rest of the body as sent, so the strict body and query rules of the other operations do not apply here.
`model` is `/` on both interfaces: the provider's slug, a slash, and the model's id in that provider's catalog, such as `openrouter/anthropic/claude-sonnet-4.6`. Because a catalog id can contain slashes, the provider is the part before the first slash. Answers name the id the request sent. The key holder may call the organization's chat models that a credential with an API key or an environment variable serves, narrowed by the credential's model allowlist and the holder's model access; models behind a subscription credential and providers with an endpoint per credential, such as Azure, are not served. `GET /api/v1/models` gives each of these models' context window, capabilities, and prices under the same `providerSlug` and `id`.
Every request is held to the key holder's model access, checked against every budget cap that binds the holder, their teams, the organization, and the key, and booked under the person and the key. The organization's input guardrails judge every text it carries before it is relayed; the answers are not filtered. A person, and each key, may have eight requests running at once.
### Read a refusal in the interface's shape
Every refusal on these routes, the authentication, organization, rate-limit, and body-size refusals included, comes in the error shape of the interface, so a vendor SDK reads it as its own. `code` carries Tale's stable code. On the OpenAI routes:
```json
{ "error": { "message": "The model endpoints are not enabled for this organization. An admin turns them on under Settings → Governance → Models → Model access.", "type": "permission_error", "param": null, "code": "MODEL_API_DISABLED" } }
```
On the Anthropic route, which also sets a `request-id` header:
```json
{ "type": "error", "error": { "type": "permission_error", "message": "The model endpoints are not enabled for this organization. An admin turns them on under Settings → Governance → Models → Model access.", "code": "MODEL_API_DISABLED" }, "request_id": "" }
```
| Status | `code` | Meaning |
| --- | --- | --- |
| **403** | `MODEL_API_DISABLED` | The organization has not turned the endpoints on. |
| **403** | `MODEL_API_FORBIDDEN` | The key holder's role cannot call them, and the holder has no live `tale:models.api` grant. |
| **403** | `MODEL_API_MODEL_FORBIDDEN` | The organization's model access blocks the model for the key holder. |
| **404** | `MODEL_API_MODEL_UNKNOWN` | No model the key holder may call has this id; `GET /api/v1/openai/models` lists them. |
| **400** | `MODEL_API_VISION_UNSUPPORTED` | The request carries images, and the model does not read them. |
| **400** | `MODEL_API_TOOLS_UNSUPPORTED` | The request offers tools, and the model takes none. |
| **400** | `MODEL_API_VENDOR_TOOL_UNSUPPORTED` | The request asks the model vendor to run a tool: OpenAI `web_search_options` or a tool whose type is not `function` or `custom`; Anthropic web search, web fetch, code execution, an MCP toolset, `mcp_servers`, or `container`. |
| **400** | `MODEL_API_GUARDRAIL_BLOCKED` | An input guardrail blocked a text in the request; nothing was sent to the model. |
| **403** | `MODEL_API_GUARDRAIL_UNSUPPORTED` | The organization's PII protection tokenizes, which a relayed answer cannot honour. |
| **503** | `MODEL_API_GUARDRAIL_UNAVAILABLE` | A moderation step failed under a fail-closed policy, or a guardrail setting cannot be read. Retry later. |
| **413** | `MODEL_API_TEXT_TOO_LARGE` | A guardrail is on, and the request carries more than 2 MB of text. |
| **429** | `BUDGET_EXCEEDED` | A budget cap that binds the key holder or the key is reached, or the request's worst case, its prompt and output cap at the model's price, does not fit what is left; a lower `max_tokens` fits more. The message names the cap, `Retry-After` names the time until a reached cap resets, and `x-should-retry: false` tells the SDKs not to retry. |
| **429** | `MODEL_API_CONCURRENCY_EXCEEDED` | The key holder, or the key, already has eight requests running. `Retry-After` asks for a two-second wait. |
| **503** | `MODEL_API_UNAVAILABLE` | Tale cannot serve the request right now, for example because the model gateway cannot serve the model. Retry shortly. |
| **400**, **413**, **422**, **429**, **503**, **529**, or **502** | `MODEL_API_UPSTREAM_ERROR` | The model vendor refused the request. The first six keep the vendor's status; any other refusal is answered as **502**. |
| **400** | `INVALID_BODY` | The body is not a request the route can relay, such as audio input or output, a role it does not relay, an image URL that is neither `data:` nor `https:`, more than eight answers (`n`), a file stored in the vendor account (`file_id`, a `file` source), a `service_tier` other than the default, or `store: true`. The message names the field, and so does `param` on the OpenAI shape. |
| **413** | `BODY_TOO_LARGE` | The body is over 32 MiB. |
## Search a project's files
Use the project search URL when results must come from one project. It searches only that project's indexed files and requires read access, including for an archived project. Hub or team documents, other projects, websites and email attachments are outside this search. Omit `corpus` or set it to `"documents"`; any other corpus or a `projectId` body field returns **400**.
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//knowledge/search" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "query": "Q1 filing deadline", "limit": 10 }'
```
### Interpret search scores and failures
The body requires `query` (trimmed before it is checked) and also accepts `limit` (1–50, default 10) and `minSimilarity` (0–1). `limit` is the size of the page, not of the search: the platform fuses a wider candidate pool, checks every candidate against its live document, and only then cuts the page — so `limit: 1` returns the best readable passage (at equal `fusedScore` a passage the keyword leg ranked precedes one only the vector leg found, then the lower row identity — an exact term is stronger evidence than a nearest neighbour).
`minSimilarity` floors the dense (vector) leg only, before fusion; there is no default on this endpoint, so without it the nearest passages answer however weak, and the keyword leg is never floored (the built-in assistant's search applies the organization's configured floor instead — `minSimilarity` in its [`embedding.json`](https://docs.tale.dev/self-hosted/configuration/data-residency#the-organizations-embedding-model), 0.45 when unset). Hits come back in fused order, best first.
`fusedScore` is that order key and nothing more: Σ 1/(60+rank) over the legs that ranked the passage, divided by the best possible for that many legs — a rank, so the best candidate of a one-leg search scores 1.0 however weak the leg found it; comparable only within one response, never a confidence, and not stable across searches. Every candidate is checked against its live document before fusion, so a passage you cannot see never holds a rank.
What you can threshold on is `similarity` — the dense leg's cosine on the embedding model's own 0..1 scale, for the hits that leg ranked; a hit only the keyword leg found carries `similarity: null` and its `keywordScore` — keep those, an exact identifier or phrase match is stronger evidence than any cosine (in code: `hits.filter(h => h.similarity === null ? h.keywordScore !== null : h.similarity >= floor)`); a scale, not a calibrated confidence: a high similarity does not prove relevance, and an unfamiliar or misspelt token can score above a term that appears in the text, so a hit the keyword leg did not also match (`matchedLegs` without `documents:keyword`) deserves a second look before you trust it — beside `keywordScore` (the BM25 weight, unbounded, `null` when only the vector leg found it) and `matchedLegs` (`documents:keyword`, `documents:dense`, `web:keyword`, `web:dense` — which legs ranked it; `legs` is their count, 2 when the keyword and the vector leg agreed).
Each hit carries its passage, its leg `score` (the first matched leg's own number) and its `source`; `diagnostics.cached` and `diagnostics.reranked` are always `false` — Tale ships no semantic cache and no reranker, and both flags stay for compatibility; `diagnostics.legs` names every leg that ran with the admitted candidates it contributed, `0` when it ran and nothing survived (`documents:dense: 0` is a vector leg that found nothing in scope, never a missing one), and `diagnostics.dense` is `false` only when the corpus could not serve the vector leg at all, as `diagnostics.bm25` is for the keyword index; passages carry no control characters other than tab, line feed and carriage return; a passage repeated inside one document — an export of one line, a templated report — is indexed once, through its first occurrence, so a duplicate-heavy file neither crowds the vector leg nor ranks its copies one by one; a documents hit also carries `source.documentId` beside the blob `ref` the index keys it by: for a Hub hit (`source.projectId` null) it is the id `GET /api/v1/documents/{id}` takes, for a project hit the file id the project routes take (`GET /api/v1/projects/{projectId}/files/{documentId}/content`, `DELETE .../files/{documentId}`) — `/api/v1/documents/{id}` returns **404** for a project file.
A missing embedding model returns **409**, `EMBEDDING_NOT_CONFIGURED`; so does an embedding provider that refuses for account reasons — a spent balance, a spend limit, or a plan that excludes the model — as **409**, `EMBEDDING_CREDIT_EXHAUSTED`; a credential the provider rejects, or one it refuses the model, is **409**, `EMBEDDING_CREDENTIAL_REJECTED` — fix the provider settings. The same **409**, `EMBEDDING_CREDENTIAL_REJECTED`, answers when the platform has no usable credential to send at all: the provider has no default credential, or the one the embedding settings name was deleted, disabled or cannot be read; `error` names which. None of these is a rate limit: no wait lifts them, an admin has to act. Any other provider failure returns **503**, `EMBEDDING_UPSTREAM_ERROR`, with `Retry-After` — retry that one with backoff.
To search visible non-project Hub and team documents or registered websites instead, use `POST /api/v1/knowledge/search` with `corpus` set to `"documents"`, `"web"` or `"all"` (the default). That URL excludes project files and email attachments. Both URLs find file-backed documents only — a document created with inline `content` never indexes.
## Mirror an external system into a project
The Projects group is built for an unattended worker that mirrors an external system — a CRM, a practice-management tool — into Tale: find or create the client's project, prepare its folders, upload files, verify. Every call acts as the key holder: a project that user cannot see returns as if it did not exist, and writes need an editing role (Editor or above — Member is read-only here) plus edit access on the project.
These routes, and the Tasks routes below, refuse to guess the organization: a key whose user belongs to several organizations must send `X-Organization-Slug` on every call — a request without it returns **400**. Mint machine keys for a dedicated user with exactly one membership and the question never comes up; the examples keep the header anyway — it is always membership-checked, never ignored.
### Find or create the project
A project carries its audience in `teamIds` — the teams that may see it; empty means the whole organization. Read the ids from `GET /api/v1/teams` (every team, by name, with `member: true` on the ones the key holder belongs to — a key holder who is not an organization admin may only name those), send them on `POST /api/v1/projects` or replace the whole set with `PATCH /api/v1/projects/{id} { "teamIds": [...] }` (an admin verb). A repeated id collapses to one, re-asserting the audience the project already carries is a no-op that leaves `updatedAt` alone, and an archived project refuses the change like every other write (**403**, `PROJECT_ARCHIVED`) unless the same body restores it.
`externalItemId` is your key, not Tale's — an opaque string (your CRM's record id), unique per organization, never interpreted by the platform. It is stored and compared after NFC normalization and trimming, so a key handed over in NFD by a macOS filesystem finds the project a worker created in NFC from a CSV, and a trailing newline from a shell variable never makes a second project.
A legacy row whose key differed from another's only in normalization was released by the platform — its `externalItemId` is empty, the row that held the canonical spelling kept the key — so re-key it with `PATCH /api/v1/projects/{id}` when it is the one you meant. Look it up first; the lookup returns at most one project, and a match the key's user cannot see looks exactly like no match:
```bash
curl -sS --compressed "https://your-host.example.com/api/v1/projects?externalItemId=crm-4711" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "projects": [] } — or [ { "id": "...", "name": "ACME Ltd", "externalItemId": "crm-4711" } ]
```
A match carries `archivedAt` when the project is archived — decide what your worker does with that case before it happens. Without `externalItemId`, the same route lists every project the key's user can see, newest first, keyset-paged like every other list — `{projects, isDone, continueCursor}`; pass `continueCursor` back as `?cursor=` until `isDone` (while more pages remain the answer also carries `cursor`, the same token under its pre-1.5 name — deprecated, read `continueCursor`; a lookup returns `isDone: true` with an empty `continueCursor`) — with archived projects left out unless you ask for them (`?archived=include`, or `?archived=only`).
Every row carries `createdAt` and `updatedAt`, so a worker can reconcile what it created:
```bash
curl -sS --compressed "https://your-host.example.com/api/v1/projects?limit=50" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "projects": [ { "id": "...", "name": "ACME Ltd", "externalItemId": "crm-4711", "createdAt": 1774..., "updatedAt": 1774... } ], "isDone": true, "continueCursor": "" }
```
An empty lookup means create:
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "name": "ACME Ltd", "externalItemId": "crm-4711" }'
# → 201 { "project": { "id": "...", "name": "ACME Ltd", "key": "ACME", "externalItemId": "crm-4711" } }
```
`key` (the task-identifier prefix) and `description` are optional — the key derives from the name when omitted. A second create with the same `externalItemId` — the same after NFC normalization and trimming — returns **409**; the same string in another organization is fine, uniqueness is per organization. A key that is blank once trimmed returns **400**, `INVALID_BODY`.
An explicit project `key` contains 2–6 letters or digits, starting with a letter, and is normalized to uppercase; invalid keys answer **400**, without truncation. A name that yields no valid key creates a keyless project. A derived key that collides is re-derived until it is free; an explicit key that collides returns **409**, `PROJECT_KEY_TAKEN` — supply an unused one. The same `externalItemId` twice is **409**, `PROJECT_DUPLICATE_EXTERNAL_ID`.
### Create folders
Folder creation is get-or-create: the same name under the same parent — compared without regard to case, so `inbox` and `INBOX` are one folder — returns the existing folder with its stored name and `created: false` (**200**) instead of a duplicate, so a worker re-runs its setup step blindly after a crash; two workers creating the same folder at once get one folder.
A name is a name, never a path: a `/` or `\`, a control character, `.` or `..` returns **400**, `FOLDER_NAME_INVALID`, the sentence naming the rule broken and `data.issues` naming `name` — the same rule a file name follows, and like a file name the folder name is stored trimmed and NFC-normalized — and `parentId` is either a folder of this project or omitted for a root folder (a blank one returns **400**, `INVALID_BODY`). Folder names carry no platform-reserved meanings — the layout is yours:
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//folders" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "name": "2026-Q1" }'
# → 201 { "folder": { "id": "", "name": "2026-Q1" }, "created": true }
```
`parentId` (a folder of this project) nests deeper; omit it for a root folder. A name is at most 128 characters, trimmed, and never a path — `a/b`, `.` and `..` answer **400**, `FOLDER_NAME_INVALID` — and a folder 20 levels deep takes no child (**400**, `FOLDER_DEPTH_EXCEEDED`).
The tree reads back one level at a time: `GET .../folders` lists the root folders, `GET .../folders?parentId=` the children of one folder, and every folder carries its `parentId` (`null` at the root); `GET .../folders/{folderId}` resolves a single folder — the `folderId` every file in `GET .../files` carries — so a worker that did not build the tree can still discover it, and a path is the parent chain walked upward. A `parentId` or `folderId` that is not a folder of this project returns **404**, `FOLDER_NOT_FOUND`.
### Upload a file in two steps
Two REST calls surround one direct object-store upload: prepare the handoff, transfer the bytes, then bind them to the project. The shell example requires `jq`, an existing project folder, and a local file whose extension and MIME type are allowed. Run each command only after the previous one succeeds.
```bash
: "${TALE_URL:?Set TALE_URL to your Tale origin}"
: "${TALE_API_KEY:?Set TALE_API_KEY}"
: "${TALE_ORG_SLUG:?Set TALE_ORG_SLUG}"
: "${TALE_PROJECT_ID:?Set TALE_PROJECT_ID}"
: "${TALE_FOLDER_ID:?Set TALE_FOLDER_ID to a folder in this project}"
: "${FILE_PATH:?Set FILE_PATH to an existing local file}"
: "${FILE_MIME:?Set FILE_MIME, for example application/pdf}"
FILE_NAME=$(basename "$FILE_PATH")
FILE_SIZE=$(wc -c < "$FILE_PATH" | tr -d ' ')
UPLOAD_BODY=$(jq -n --arg name "$FILE_NAME" --arg type "$FILE_MIME" \
--argjson size "$FILE_SIZE" '{fileName:$name,contentType:$type,size:$size}')
UPLOAD_JSON=$(curl --fail-with-body --silent --show-error \
"$TALE_URL/api/v1/projects/$TALE_PROJECT_ID/uploads" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG" \
--header 'Content-Type: application/json' --data "$UPLOAD_BODY")
UPLOAD_ID=$(printf '%s' "$UPLOAD_JSON" | jq -er '.uploadId')
UPLOAD_URL=$(printf '%s' "$UPLOAD_JSON" | jq -er '.url')
FILE_REF=$(printf '%s' "$UPLOAD_JSON" | jq -er '.s3Ref')
printf '%s' "$UPLOAD_JSON" | jq '{uploadId,method,expiresAt,maxBytes}'
```
#### Send the bytes before binding
The returned `url` is a presigned URL for a direct `PUT` to object storage. Send no `Authorization` header: the signature already authenticates the request, and adding a second authentication method is rejected. If preparation included `contentType`, the PUT's `Content-Type` must match it exactly. Omitting `contentType` during preparation leaves no required content-type header. Use the returned `s3Ref` as `fileId` when binding the upload.
Include `fileName` during preparation, for example `"fileName": "ledger-2026-q1.pdf"`, to check the format and organization policy before requesting a signed URL. A rejected file type or a name without an extension returns **400** with `UPLOAD_POLICY_REJECTED` or `UNSUPPORTED_FILE_TYPE`. Specifying a MIME type does not replace the required extension.
`maxBytes` reports the permitted size for the declared type: at most 100 MiB (104,857,600 bytes), or a stricter organization limit. Include the optional `size` to check the planned upload before transferring it. Exceeding the platform cap returns **400** `FILE_TOO_LARGE`; exceeding organization policy or volume quota returns **400** `UPLOAD_POLICY_REJECTED`. `data.limitBytes` identifies the limit.
This pre-check avoids transferring an oversized file. Binding still checks the actual stored byte count against the current limits. It does not compare that count with the declared `size`, so understating the declaration cannot bypass the upload limit.
Transfer the bytes to the returned URL. Do not send the Tale API key to this URL: the signature already authenticates the object-store request. Wait for a successful upload before binding.
```bash
curl --fail-with-body --silent --show-error --request PUT "$UPLOAD_URL" \
--header "Content-Type: $FILE_MIME" \
--upload-file "$FILE_PATH"
```
```bash
BIND_BODY=$(jq -n --arg upload "$UPLOAD_ID" --arg ref "$FILE_REF" \
--arg folder "$TALE_FOLDER_ID" --arg name "$FILE_NAME" \
'{uploadId:$upload,fileId:$ref,folderId:$folder,fileName:$name}')
curl --fail-with-body --silent --show-error \
"$TALE_URL/api/v1/projects/$TALE_PROJECT_ID/files" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG" \
--header 'Content-Type: application/json' --data "$BIND_BODY"
```
The binding response is `201 {file}` and includes the new document ID. Keep `file.id` for later listing, download, indexing, or deletion; it is different from `uploadId` and `s3Ref`.
#### Handle expiry, format policy, and abandoned uploads
The stored `mimeType` is resolved from the file extension, and downloads use it as `Content-Type`. A multipart media type or upload hint does not override the extension-based policy.
`uploadId` is single-use. Both it and the signed URL expire after 30 minutes; `expiresAt` is their shared deadline. After an interrupted upload, prepare a new one if that deadline has passed.
`fileName` must be a plain filename. Tale trims it and applies NFC normalization; path separators or control characters return **400**. An allowed extension is required during preparation and binding: `CON` and `attachment-4711` return `UNSUPPORTED_FILE_TYPE`, even when `contentType` is supplied.
Binding rechecks the upload policy. Supported extensions are `pdf`, `doc`, `docx`, `odt`, `ppt`, `pptx`, `xls`, `xlsx`, `csv`, `txt`, `md`, `json`, `yaml`, `yml`, `py`, `jpg`, `jpeg`, `png`, `gif`, `webp`, and `ac2` (Banana accounting ledger), subject to the organization's narrower policy. The `UNSUPPORTED_FILE_TYPE` message includes the sorted format list. Unsupported formats and excessive sizes return **400** with the relevant reason code.
If the bytes have not reached object storage, binding returns **404** `BLOB_NOT_FOUND` and leaves the upload ID usable. Complete the PUT, then bind the same upload again.
Without configured object storage, both preparation and binding return **503** `OBJECT_STORE_UNCONFIGURED`. Unbound blobs are cleaned up automatically: they become eligible 24 hours after the 30-minute upload deadline, and a later upload-preparation request in the same organization removes a batch of expired upload records and blobs. This also covers rejected bindings and clients interrupted between transfer and binding.
Until cleanup, these bytes remain in the bucket but do not appear in file lists or count against a quota.
#### Choose whether to index the file
Files that enter through this endpoint are project working material, not organization knowledge: they skip knowledge indexing by default (`skipRagIndexing` defaults to `true` on the bind; pass `false` to opt in), and they never appear under `/api/v1/documents` — that family stays the knowledge hub's surface.
A skipped file is still listed by the project's chat and reads **Not indexed** on the project's Knowledge tab: a search never finds it until it is indexed (**Index now** on its row, `POST /api/v1/projects/{id}/files/{documentId}/retry-indexing` from REST — the same core and the same 10-per-user-per-minute budget as the Hub document's retry, answering `{"status": "indexing"}` and lifting the opt-out, or `skipped` with its `reason` — or a bind with `skipRagIndexing: false`), but the assistant reads a plain-text file of up to 4 MiB on request — `rag_fetch` on its id serves the bytes as text rather than answering that the file is unindexed.
### Verify what landed
Choose the file scope with `folderId`: omit it for all files in the project, use `folderId=root` for unfiled files at the project root, or pass a folder ID for that folder’s files. A folder outside the project returns **404**, `FOLDER_NOT_FOUND`.
```bash
curl -sS --compressed "https://your-host.example.com/api/v1/projects//files?folderId=" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "files": [ { "id": "...", "fileName": "ledger-2026-q1.pdf", "folderId": "", "mimeType": "application/pdf", "size": 48213, "indexing": { "status": "skipped" }, "createdAt": 1774... } ], "isDone": true, "continueCursor": "" }
```
Each row carries the landed `size` and the file's `indexing` state in the vocabulary `GET /api/v1/documents` speaks: a file bound with the default `skipRagIndexing: true` reads `skipped`, and project search does not find it until it is indexed — `POST .../files/{documentId}/retry-indexing`, then poll the single-file read until indexing settles. The listing returns `{files, isDone, continueCursor}`: while `isDone` is `false`, pass `continueCursor` back as `?cursor=` unchanged (it is an opaque signed token) and cap the page with `?limit=` (max 100); while more pages remain the answer also carries `cursor`, the same token under its pre-1.5 name — deprecated, read `continueCursor`.
To monitor one file, read `GET /api/v1/projects/{id}/files/{documentId}`. It returns `{file}` with `id`, `fileName`, `folderId`, `mimeType`, `createdAt`, and `size` in bytes (`null` when unknown), plus indexing state when available. Send its `ETag` as `If-None-Match` for `304` while unchanged; after `POST .../retry-indexing`, poll this row until indexing settles. You do not need to rescan the file list. Missing, trashed, non-file, or out-of-project records return the opaque `404 FILE_NOT_FOUND`.
```bash
curl --fail-with-body --compressed "$TALE_URL/api/v1/projects//files/" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG"
```
### Delete what you no longer need
Nothing this endpoint creates has to stay forever. A file goes with `DELETE .../files/{documentId}` — permanently: its document row, its search-corpus rows and its blob are purged through the same lane every hard delete uses, so the answer is **204** or a refusal, never a silent no-op:
```bash
curl -sS --compressed -X DELETE "https://your-host.example.com/api/v1/projects//files/" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 204
```
A folder goes with `DELETE .../folders/{folderId}`, and everything beneath it goes too — every file in it and in its subfolders is purged first (their corpus rows released at once, so project search stops matching them), then the subtree. A protected controlled record or a legal hold anywhere beneath refuses the whole delete with **409** before anything is removed; a purge the object store could not finish returns **503**, `PURGE_INCOMPLETE`, with nothing removed — retry it. Both deletes need edit access to an active project; a file or folder of another project, or one already gone, returns **404** (`FILE_NOT_FOUND`, `FOLDER_NOT_FOUND`).
#### Archive, rename, or delete the project
The project itself has a lifecycle too, for organization admins (**403**, `ROLE_FORBIDDEN`, for anyone else). `PATCH /api/v1/projects/{id}` with `{ "archived": true }` archives it — it stays readable through this endpoint, refuses every write with **403**, `PROJECT_ARCHIVED`, keeps its `externalItemId` taken, and `{ "archived": false }` restores it.
The same `PATCH` carries the identity a mirror propagates when the source record changes: `name` (trimmed, never blank), `description` (`null` clears it) and `externalItemId` (stored NFC-normalized and trimmed; `null` releases the key, another project's key returns **409**, `PROJECT_DUPLICATE_EXTERNAL_ID`, with the key in `data`) — for editors with project edit access on an active project, every field optional and at least one required. A body that restores and renames applies the restore first, one that renames and archives applies the archive last; renaming an archived project the body does not restore returns **403**, `PROJECT_ARCHIVED`.
When "ACME Ltd" becomes "ACME Group" in the CRM, `{ "name": "ACME Group" }` is the whole move — nothing under the project is touched. `DELETE /api/v1/projects/{id}` removes it and frees the key: by default a cascade — every document expires into the retention pipeline, your own chats are trashed, every task is retired and its live runs cancelled — or, with the body `{ "mode": "detach" }`, the documents and chats are released into the organization instead. The project's run history goes with it either way, finished runs included — unlike an automation delete, which keeps its runs.
Agents and folders go with the project either way, and a cascade draws on the same per-user budget the app's delete does (5 per minute):
```bash
curl -sS --compressed -X DELETE "https://your-host.example.com/api/v1/projects/" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 204
```
The delete is refused with **409**, before anything is written, while an automation is installed in the project — `PROJECT_HAS_BOUND_AUTOMATIONS`, with `data.automations` naming them; uninstall each via `DELETE /projects/{id}/automations/{name}` first — while a cascade would destroy a controlled record that is in review, approved or retains an approved version (`PROJECT_HAS_PROTECTED_RECORDS`, `data.documents` naming them), or while a legal hold covers one of its documents (`PROJECT_LEGAL_HOLD`).
## Materialize a task, then run it
The Tasks group turns an external item into a task on a project's board, starts a deployed workflow on it and reports back. A project-bound automation must be installed in this project first. Installing is idempotent: **201** on the first call, **200** when the binding exists. It requires the developer capability and edit access to an active project. Bind ahead of time if the worker's user lacks those permissions:
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//automations/vat-return" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{}'
# → 201 { "name": "vat-return", "added": true }
```
`GET /api/v1/projects/{id}/automations` lists the automations installed in that project. An automation without any project bindings can also run in an accessible project when the caller has the required edit permissions, but it is not part of that installed list. `DELETE /api/v1/projects/{id}/automations/{name}` uninstalls one again — **204**, or **404** `AUTOMATION_NOT_INSTALLED` when it was not installed there — under the same developer capability and edit access.
### Create or update the mirrored task
Task creation is idempotent per `(projectId, externalSystem, externalId)`: the first call creates (**201**, `created: true`) a task in `backlog` — the mirror's intake column, where the app's own Create task defaults to To do — and a repeat returns with the same task (**200**, `created: false`). Both keys are stored and compared after NFC normalization and trimming — the same rule as a project's `externalItemId` — so a padded or differently normalized repeat is still the same task, and a key that is blank once trimmed returns **400**. Take `projectId` from the URL; sending it in the body returns **400**. Every key holder who can read an active project may create a task, Members included (contract 3.6.0). A repeat changes the task it names, so it needs the right to change that task: an Editor or higher, or the member who created the task or is its person assignee. Anyone else gets **403**, `RBAC_FORBIDDEN`, and nothing is written.
For `externalSystem: "github"` or `"glitchtip"`, `externalState` never changes the Tale task's status. New tasks enter `backlog`; closing, resolving or reopening the upstream issue leaves local progress unchanged. The issue import automations display source status separately on the task.
For other source systems, `externalState` mirrors the source item's lifecycle: `closed` parks the task at `in_review` for a person to complete (only the workflow engine itself lands a close at `done`), and `open` reopens a task the mirror closed — one it parked at `in_review`, or a `done` one — back to `backlog`; a park a person or an agent made is theirs, `open` leaves it, and any move through the board ends the mirror's claim on a park it made. A cancelled task stays cancelled either way. A pending review owned by an agent keeps its status and reviewer. A custom-source sync records an upstream close or reopen separately, without approving or withdrawing that review.
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//tasks" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "externalSystem": "crm", "externalId": "case-991", "title": "Prepare the Q1 filing" }'
# → 201 { "task": { "id": "", "created": true } }
```
Repeating an active task’s external reference updates its title and description; omitting `description` clears it. Labels change only when supplied. An archived task stays unchanged. The task id stays the same, and `runWorkflowSlug` does not start another run on that repeat. Keep the repeated payload stable when retrying after a lost response.
### Bind a setup folder and choose automation ownership
`description`, `labels`, `externalUrl`, and `setupFolderName` are optional. `title` takes up to 200 UTF-16 code units (most emoji count as 2) and `description` up to 20,000; `labels` takes up to 50 names of up to 50 code units each. `externalUrl` must be an absolute `http(s)` URL. A value past its limit, or another scheme, returns **400** rather than a silently altered task. `setupFolderName` binds the task to one of the project's root folders by name — matched without regard to case — and stores that folder's id as the task's `externalUrl`: the Setup-folder binding a folder-driven automation reads off its task input, resolved again on every repeat.
A name no root folder of the project carries returns **400**, `SETUP_FOLDER_MISSING`, and nothing is created; sent beside `externalUrl`, it returns **400**, `INVALID_BODY`. Labels keep their spelling: a name is trimmed and NFC-normalized, matched against the project's catalog without regard to case, created with the spelling you sent when it is new, and read back as stored in the order you sent — so `["Bug", "P1"]` reads back as `["Bug", "P1"]`, while `["bug"]` on a project that already has `Bug` wears that existing label (two names differing only in case are one label, never two). Only an Editor or higher on the project creates labels this way; any other key holder names labels the catalog already has, and a new name returns **400**, `TASK_LABEL_UNKNOWN`, with nothing written.
Send `automationSlug` when the task belongs to an automation: it becomes the assignee, and the task modal's work panel — the Start button, run progress, and the operator questions a run asks — keys on that ownership (a later re-pick fills a missing attribution, but never overwrites an assignee). `runWorkflowSlug` starts a deployed workflow on a newly created task in the same call — the run starts inline, so the response carries its `runId` (the run id to poll; `executionId` repeats it and is deprecated), or `runId: null` if the slug has no deployed version or starting fails after the task is committed. A null run ID does not undo the saved task; inspect and correct the workflow, then use the explicit start route.
Start explicitly instead when you want to name the workflow in a separate call. The owning `automationSlug` must name an automation that exists — **404**, `AUTOMATION_NOT_FOUND`, otherwise — with a deployed version: one that is saved but not deployed returns **409**, `AUTOMATION_NOT_DEPLOYED`. A workflow bound to other projects returns **403**.
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//tasks//start" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "workflowSlug": "vat-return" }'
# → 200 { "started": true, "runId": "", "executionId": "" }
```
### Check whether a task run started
Starting requires the right to change the task — an Editor or higher, or the member who created it or is its person assignee (**403**, `RBAC_FORBIDDEN`, otherwise) — in an active project, and an active task — an archived task returns **403**, `TASK_ARCHIVED` (the task read carries `archivedAt` while it is; `PATCH …/tasks/{taskId}` with `{ "archived": false }` restores it — see below). It wraps the task as `{task: ...}` and needs no additional developer capability; the run log attributes the start to your key. Poll `GET /api/v1/projects/{id}/runs/{runId}` with the `runId` (`executionId` carries the same value and is deprecated).
The answer is **200** whether or not a run started, so branch on `started`, never on the status alone: with `started: false`, `reason: "already_running"` carries the in-flight run's `runId` — a task holds at most one live run, whichever automation started it, so that run may belong to another automation (its `name` says which); poll that run. A workflow bound to other projects returns **403**, `AUTOMATION_PROJECT_FORBIDDEN`. The `workflowSlug` names the automation as `GET /api/v1/automations` lists it — the `/` form (`billing/dunning`), never the `__` spelling the URL path takes — and must name one that exists — **404**, `AUTOMATION_NOT_FOUND`, otherwise — with a deployed version: one that is saved but not deployed returns **409**, `AUTOMATION_NOT_DEPLOYED` — the same two refusals the intake gives an `automationSlug`, judged before the execute budget is charged; `reason: "not_started"` is left for the one residual case, a deployment withdrawn between that check and the start.
A task with an active project-agent run refuses a workflow start with **409** `TASK_HAS_LIVE_RUN`. Wait for the agent to finish or cancel its run before starting the workflow. The same rule prevents an agent from starting while a workflow holds the task.
New work also returns **403** `TASK_AUTOMATION_DISABLED` when an administrator has turned off task automation, or **409** `TASK_AUTOMATION_UNAVAILABLE` when its policy cannot be read. Ask an administrator to enable task automation or restore the policy. Existing runs can finish, and comments are still saved.
Concurrent starts for the same task share the one in-flight run, whichever automation they name, including requests that arrive together. This is not `Idempotency-Key` support for task starts: once that run finishes, another start can create another run. Save the returned `runId` and inspect it before retrying an uncertain start.
### Archive or restore a task
`PATCH /api/v1/projects/{id}/tasks/{taskId}` with `{ "archived": true }` archives the task — the board's own archive: it stays readable here and refuses comments and starts with **403**, `TASK_ARCHIVED` — and `{ "archived": false }` restores it. Both are idempotent, so a mirror that supersedes a task (a re-delivery that opened a new one, a source record that was cancelled) retires the old task without reading it first. It needs an **active** project and the right to change the task, as an Editor or higher or as the member who created it or is its person assignee (**403**, `PROJECT_ARCHIVED` or `RBAC_FORBIDDEN`); the task itself may be archived, which is what the restore is for. The body takes exactly `archived`; title, description and labels travel through the intake's repeat. The answer is the task as it now stands.
```bash
curl -sS --compressed -X PATCH "https://your-host.example.com/api/v1/projects//tasks/" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "archived": true }'
# → 200 { "task": { "id": "", "status": "in_progress", "archivedAt": 1789921403000, ... } }
```
### Comment and read task state
Report back and read state — the comment posts as the key holder, indistinguishable from the same person commenting in the app, @mentions included. Project readers, including Members, may comment on an active task in an active project; an archived task refuses the comment with **403**, `TASK_ARCHIVED`, the way an archived project does with `PROJECT_ARCHIVED`. Reading a task or its comments is also allowed after archival. Every task URL is judged left to right: a project that is missing or invisible returns **404**, `PROJECT_NOT_FOUND`, and only a task that is missing or belongs to another project returns `TASK_NOT_FOUND`.
```bash
curl -sS --compressed -X POST "https://your-host.example.com/api/v1/projects//tasks//comments" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-H "Content-Type: application/json" \
-d '{ "body": "Filed. Confirmation 2026-8842." }'
# → 201 { "comment": { "id": "..." } }
curl -sS --compressed "https://your-host.example.com/api/v1/projects//tasks/" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "task": { "id": "", "title": "...", "status": "in_progress", "externalId": "case-991", "labels": [], "dueDate": 1790719200000, "repeat": null, ... } }
```
A task read also carries its schedule, which is read-only on this API. `startDate` and `dueDate` are epoch milliseconds and appear only when set. When you choose these dates in the app, it writes the midnight that starts the chosen calendar day in your browser’s time zone. `repeat` is always present: `null` when the task doesn't repeat, otherwise a `TaskRepeat` such as `{ "frequency": "weekly", "interval": 2, "weekdays": [2, 4], "timezone": "Europe/Zurich", "createOn": "dueDate" }`. `frequency` is `daily`, `weekly`, `monthly` or `yearly`, and `interval` runs from 1 to 99; `weekly` lists `weekdays` from 0 (Sunday) to 6 (Saturday), `monthly` takes a `monthDay`, and `yearly` a `month` and a `monthDay`. `createOn` appears, as `"dueDate"`, only when the next task is also created at the start of the due date; without it, the next task comes when this one is done or cancelled. When a repeating task closes, in the app or through an approved review here, or its due date begins under `createOn: "dueDate"`, its next task is created in `todo`, and `repeatNextTaskId` names it from then on. Deleting that next task removes `repeatNextTaskId`, but this task still never creates another one (contract 3.3.0).
### Read comments and download deliverables
A comment's canonical `body` is trimmed and takes 1 to 10,000 UTF-16 code units (most emoji count as 2). Comment writes also accept an optional `bodyByLocale`, which comment reads return when present. Supply equivalent nonblank translations for `en`, `de` and `fr`; additional language or language-region keys such as `nl`, `it` and `de-CH` are allowed. Each value is trimmed and held to the same limit, with at most 16 locales per comment. Render the reader's exact locale, then its base language, then `en`, then `body`. Attribution remains the key holder's. A plain-text edit in Tale clears the old translations so they cannot hide the edit.
Task and workflow agents receive instructions to preserve the language established by the task title and description, falling back to the organization's default agent language. Generated title-template words, quarter identifiers, source-document languages and the run starter's UI locale do not establish the task language. The same policy applies to operator questions, resumed turns and related task creation. This is model guidance; localized progress snapshots let clients select a translation independently of the canonical task language.
Read the run output and task comments to collect the automation's results. Whether it also creates files, and where it stores them, depends on the workflow; starting a task does not by itself put files in the example quarter folder.
Comments arrive in pages, newest page first and chronological within each page. `limit` defaults to 200 and allows at most 500. While `isDone` is `false`, pass `continueCursor` unchanged as `cursor` to read older comments. It is an opaque signed token, not a page number.
The content endpoint streams the bytes itself (**200**, no redirect to follow), named by an RFC 6266 `Content-Disposition`, so a plain `curl -o` lands the file and `--fail-with-body` turns a refusal into a non-zero exit instead of a file full of JSON.
`Range` is honoured: a single byte range (`bytes=0-1023`, `bytes=1024-`, `bytes=-512`) returns **206** with `Content-Range`; a range starting at or past the end of the file — what `curl -C -` sends once the local copy is complete — returns **416** with an empty body and `Content-Range: bytes */` naming the size, so a resuming worker learns it is done; several ranges or a `Range` the server cannot read are ignored and the whole file returns **200**.
A `HEAD` returns the same headers a `GET` carries — `Content-Length`, `Content-Type`, `ETag`, `Last-Modified`, `Accept-Ranges` — without the bytes and ignores `Range`, so a poller checks for a new version with `curl -I` (not `curl -X HEAD`, which waits for a body) and sends the `ETag` back as `If-None-Match` to get **304** while nothing changed:
```bash
curl -sS --compressed "https://your-host.example.com/api/v1/projects//tasks//comments?limit=100" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: "
# → 200 { "comments": [ { "id": "...", "authorType": "agent", "body": "Return prepared — key figures…", ... } ], "isDone": false, "continueCursor": "" }
curl -sS --compressed --fail-with-body "https://your-host.example.com/api/v1/projects//files//content" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: " \
-o report.md
# → the file bytes (Content-Disposition carries the filename)
```
## Error model
API refusals normally use a flat JSON envelope. A conditional `304`, a `HEAD` response, or a refusal from an intermediary can have no JSON body:
```json
{ "error": "Automation not found", "code": "AUTOMATION_NOT_FOUND" }
```
The [compatible model endpoints](https://docs.tale.dev/develop/api-reference#model-endpoints) are the exception: under `/api/v1/openai` and `/api/v1/anthropic`, every refusal, the authentication and rate-limit ones included, takes the OpenAI or Anthropic error shape and carries the same `code` there.
`error` is a sentence for humans; `code` is the stable value to branch on — every refusal the API itself makes carries one, and the OpenAPI document lists the full set as the `Error.code` enum. The set is additive: a new code is a minor change, so treat a value you do not know as a generic refusal of the status you got. Some refusals add `data` — `issues` for a refused body, `retryAfterMs` for a rate limit, `providers` for an ambiguous model. Branch on the code where one is named below, on the status otherwise:
Read the exact error enum without a key:
```bash
curl --fail --silent --show-error "$TALE_URL/openapi.json" \
| jq -r '.components.schemas.Error.properties.code.enum[]'
```
**400: correct the request before retrying.** Invalid bodies return `INVALID_BODY` with `data.issues`. Each issue has a field path, such as `price` or `contacts.2.email`, and a short message suitable for display: `is required`, `must be a string`, `must not be blank`, `must be at most 200 UTF-16 code units`, or `must be one of "a", "b"`. Unknown keys are reported under their own names. Branch on `code`, not this human-readable wording.
Body validation rejects missing required values, incorrect types, unknown keys, malformed JSON, invalid UTF-8, NUL characters, unpaired UTF-16 surrogates, integers beyond 2^53 − 1, and numbers outside the field's permitted range. A search `limit` in the JSON body is rejected when out of range; it is not clamped. The API reads JSON regardless of `Content-Type` and does not return 415 for these bodies.
| Other 400 code | Meaning and recovery |
| --- | --- |
| `INVALID_CURSOR` | The token did not come from this list. Restart pagination without it. |
| `INVALID_LIMIT` | A query `limit` is not an integer. Valid integers outside the range are clamped instead. |
| `INVALID_QUERY` | Another query parameter is invalid. Query failures name the parameter in `data.issues`; none silently restart at page one. |
| `AUTOMATION_INPUT_INVALID` | Correct the input against the automation's `inputs` schema using `data.issues`. |
| `AUTOMATION_TRIGGER_INVALID` | Correct a trigger that cannot fire, including an impossible date such as `0 0 30 2 *`. |
| `CONTACT_IDENTITY_REQUIRED` | Preserve at least one contact identity field. |
| `DOCUMENT_RECORD_FROZEN`, `DOCUMENT_RECORD_REPLACEMENT_REQUIRED` | Follow the controlled-record replacement workflow instead of editing protected content directly. |
| `ORG_SLUG_REQUIRED` | Supply the organization for a key holder with multiple memberships. |
| `INVALID_HEADER` | Use 1–255 printable ASCII characters after trimming for `Idempotency-Key`; `data.issues` names the header. |
| `INVALID_URL` | Remove NUL bytes (`%00`) from the path or query. This check runs before routing and authentication. |
| `BODY_CHUNK_MALFORMED` | Correct malformed HTTP/1.1 chunk framing in the client or intermediary. The edge supplies its own `requestId` and no contract version. |
| `BODY_LENGTH_MISMATCH` | Under HTTP/2, the body ended before its declared `Content-Length`. The edge response has a fresh `requestId` and no `X-Tale-Api-Version`. |
- **401** — missing or invalid API key (`UNAUTHORIZED`), with a `WWW-Authenticate: Bearer` challenge.
- **403** — the holder lacks the required role (`ROLE_FORBIDDEN`, `KNOWLEDGE_ENTRY_FORBIDDEN`) or project edit access, may not change another member's task (`RBAC_FORBIDDEN`), a document's or project's `teamIds` names a team the holder is not in (`TEAM_ACCESS_DENIED`), a skill save would share with the whole organization while the organization reserves that (`SKILL_PUBLISH_FORBIDDEN`), the project or task is archived for a requested mutation (`PROJECT_ARCHIVED`, `TASK_ARCHIVED` — a project's `teamIds` included), an automation cannot run in this project, or `X-Organization-Slug` names an organization the key holder is no member of (`ORG_FORBIDDEN`).
- **404** — the resource is absent, invisible to the holder, owned by another thread user, or belongs to a different project than the URL names; each family names its own code (`PROJECT_NOT_FOUND`, `DOCUMENT_NOT_FOUND`, `THREAD_NOT_FOUND`, …), an `X-Organization-Slug` that names no organization returns `ORG_SLUG_INVALID`, and an unknown route returns `NOT_FOUND` — once the key is checked: keyless, the endpoint's **401** comes first, so a path this endpoint never served (`/api/v1/openapi.json`, say) returns **401** without a key and **404** with one; the document itself lives at `/openapi.json`, outside the endpoint and keyless.
- **405** — the route exists, but not for that verb (`METHOD_NOT_ALLOWED`); `Allow` lists the verbs it serves.
- **409** — the state refuses the action: no deployed version, a bound automation called without a project URL, a run still in flight on delete (`RUN_ACTIVE`), an `Idempotency-Key` reused with a different body (`IDEMPOTENCY_KEY_REUSED`), an archived thread or a turn already running, a duplicate — a contact's `email` or `externalId` (`CONTACT_DUPLICATE_EMAIL`, `CONTACT_DUPLICATE_EXTERNAL_ID`), a product's `name` or `externalId` (`DUPLICATE_PRODUCT_NAME`, `DUPLICATE_PRODUCT_EXTERNAL_ID`), a knowledge entry's topic (`KNOWLEDGE_ENTRY_DUPLICATE`), a project's `externalItemId` (`PROJECT_DUPLICATE_EXTERNAL_ID`) —, a superseded knowledge entry (`KNOWLEDGE_ENTRY_SUPERSEDED`), a stale `expectedUpdatedAt` (`CONTACT_STALE`, `PRODUCT_STALE`, `DOCUMENT_STALE`), a document that backs an active knowledge entry (`DOCUMENT_HAS_KNOWLEDGE_ENTRY` — delete or update the entry instead), a delivery retry on one that is not dead-lettered (`DELIVERY_RETRY_UNAVAILABLE`), a newer conversation content snapshot bound to a trashed contact (`CONVERSATION_CONTACT_TRASHED`), or search without an embedding model.
- **412** — a precondition failed and nothing was written: `If-Match` on a skill whose `SKILL.md` changed since you read it, or with nothing stored (`SKILL_STALE` — `data.etag` names the current tag, `null` when nothing is stored); `If-None-Match: *` on a skill slug that already has a bundle (`SKILL_EXISTS`); a document `If-Match` that no longer matches its read representation (`PRECONDITION_FAILED`, with the current tag in `data.etag`).
- **413** — the body is too large (`BODY_TOO_LARGE`; the sentence names the cap): every JSON body at its cap (1 MiB unless the operation says otherwise — the caps are listed above), the webhook trigger at its 256 KiB (262,144 bytes) cap. An uploaded file that breaks the size or type policy is refused at the bind with **400** and a reason code instead.
- **422** — a skill bundle the file layer cannot read — a planted symlink, a file over the 4 MiB staging cap, a `SKILL.md` that does not parse (`SKILL_MALFORMED`): answered by the reads and, on `PUT`, only for the bundle already stored under the slug — the body you send is validated as **400** (`INVALID_BODY`, `INVALID_SKILL`), so a JSON body alone never provokes it.
- **429** — rate limit exceeded (`RATE_LIMITED`). `error` describes the wait; `requestId` identifies the request. Wait for `Retry-After` in whole seconds or `data.retryAfterMs` in milliseconds before retrying. See [Rate limits](https://docs.tale.dev/develop/rate-limits).
| Status | Meaning and recovery |
| --- | --- |
| **414** | the request URL (path and query) exceeds 32 KiB (`URI_TOO_LONG`); the envelope carries a `requestId`. |
| **408** | the request did not finish arriving within 15 minutes, headers and body together (`REQUEST_TIMEOUT`); the envelope carries a fresh `requestId` (the timed-out request never got one of its own) and the connection is closed — retry on a faster link or in smaller pieces. |
| **431** | the request headers as a whole exceed the edge's 64 KiB budget; answered without the envelope and with no `X-Request-Id` (the edge's parser writes it before any route runs) on HTTP/1.1 (which lets a few KiB of slack through first — a URL or header just past the budget still reaches the platform and is judged by its rules, a 66 KiB URL answering **414**), and on HTTP/2, where the budget is exact, by closing the connection. |
| **500** | internal error (`INTERNAL_ERROR`); the envelope carries a `requestId` to quote when you report it. |
| **503** | a dependency the request needed is down: the platform's database, while it restarts or cannot be reached (`DATABASE_UNAVAILABLE`, with `Retry-After` and a `requestId` — on any operation and the webhook trigger, the key check included, so a working key is never refused with **401** meanwhile), the embedding provider (`EMBEDDING_UPSTREAM_ERROR`, with `Retry-After`), the object store behind a file download (`OBJECT_STORE_UNAVAILABLE`, with `Retry-After`) or a deployment without one (`OBJECT_STORE_UNCONFIGURED`), a document purge that could not finish (`PURGE_INCOMPLETE`), or an object store that accepted a knowledge-entry write and never answered it within 30 seconds (`KNOWLEDGE_ENTRY_STORE_TIMEOUT` — nothing is written) — retry with backoff. |
| **502**, **503**, **504** | answered at the edge while the platform restarts or cannot be reached (`UPSTREAM_UNAVAILABLE`, with `Retry-After`, a fresh `requestId` and no `X-Tale-Api-Version`), on every machine endpoint — `/api/*`, `/events`, `/status.json`, `/openapi.json`, `/.well-known/*`; a browser navigation gets the maintenance page instead — retry with backoff. |
Unbinding a trigger from an existing automation (`DELETE .../triggers`) returns **204** whether or not a trigger was bound; an unknown automation returns **404**. Deleting a resource returns **404** when it is absent, including a contact already moved to trash, a product already deleted or a knowledge entry already deleted. A contact's `DELETE` is a move to the trash (`POST /api/v1/contacts/{id}/restore` brings it back); a product's `DELETE` is permanent — products have no trash and no restore, and the name and `externalId` are free for a new product at once, which is a new row with a new id. It takes the product's uploaded image with it (as does a `PATCH` that replaces or removes `imageUrl`) unless another product still shows the same upload; under an active legal hold — the organization's, or a custodian hold on the image's uploader — the delete is refused with **409**, `LEGAL_HOLD_ACTIVE`, and a patch keeps the superseded image. Deleting an active knowledge entry retires every version of its topic and moves the Hub document behind it to the trash — and drops that document's passages from the search corpus at once; deleting that document directly is refused.
Cancelling an unknown run also returns **404**; `{cancelled: false}` means the run exists but has already finished; the accompanying `status` identifies its terminal state.
## Versioning
Three numbers describe a running instance, and they mean different things. The build (`GET /api/health` returns it) is the deployment's release. The REST prefix, `/api/v1/`, is the compatibility line: a route under it is served until a `/api/v2/` exists and the retirement of `/api/v1/` has been announced in the release notes at least two minor releases ahead, with `Deprecation` and `Sunset` headers on the retiring routes in the meantime.
The contract's own version is `info.version` in the OpenAPI document at `/openapi.json` — semver for the wire: a minor bump for an additive change (a new operation, field, header or error code), a major one for a removal or a changed meaning — and every response names the version the instance implements in `X-Tale-Api-Version`, so a client that pins to a version can tell when the instance moved. The document describes the routes and request and response schemas served by the running instance, with `servers` set to that instance; `/docs` renders it.
Use it as the contract for your client, and read each release's notes under **API contract changes** — every wire change to this surface is listed there with the old and the new behaviour ([release notes format](https://docs.tale.dev/self-hosted/operate/release-notes/format)). Published notes are available on [GitHub Releases](https://github.com/tale-project/tale/releases).
A few endpoints live outside that document on purpose: `GET /api/health` is the unauthenticated liveness probe (`{"status":"ok","version":""}`), `GET /status` and `/status.json` the deployment's own [status page](https://docs.tale.dev/develop/status-page), `/openapi.json` and `/docs` the contract itself; the [WebDAV](https://docs.tale.dev/develop/webdav-api) and OpenID Connect surfaces (above) speak their own protocols.
## Where this fits
This page is the REST half of the outside surface. The [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) exposes the same platform to MCP clients — automation authoring lives there, not in REST. The [Webhooks page](https://docs.tale.dev/develop/webhooks) covers the inbound trigger that starts runs without a key. If you are building inside the product — project agents, automations — the [Platform tab](https://docs.tale.dev/platform) is your day-to-day; this page is for outside. To bring Tale into opencode, Claude Code, or a shell script, as their model provider through the [compatible model endpoints](https://docs.tale.dev/develop/api-reference#model-endpoints) or as a source of knowledge and tools, read [Use Tale from your editor or a script](https://docs.tale.dev/develop/use-tale-from-your-editor).
# Contributor Compose files
Source: https://docs.tale.dev/develop/compose-files
Use the repository’s Compose files to develop or test Tale from source. For the usual local workflow, [run the app and backend natively with Docker dependencies](https://docs.tale.dev/develop/contributor-setup). Use the container workflow below when your change needs to exercise the development images.
Packaged self-hosted installations use the CLI-generated stack described in [Quickstart](https://docs.tale.dev/self-hosted/install/quickstart). The source-tree overlays have development ports and mounts; review them before exposing a host publicly.
Choose one workflow for the change you are testing. Starting native development and the container frontend on the same port produces a conflict, not a second isolated instance.
## Start the container development workflow
Run from the repository root with the pinned Bun version and Docker Compose available:
```bash
bun install
bun run docker:dev
bun run docker:dev:logs
```
The `docker:dev` wrapper prepares the sandbox runtime image and network, generates an environment overlay, and starts the base, development and docs files together. Prefer that entry point to copying only its final Compose command: the preparation is part of the workflow. The generated environment overlay forwards most host variables into the platform container, so review the environment you run it from.
Stop the live log stream with `Ctrl-C`. Use `bun run docker:dev:down` to stop this stack. Keep data volumes and the existing environment when you want to resume with the same instance. A second worktree needs its own ports, container identities and storage to run independently.
## Choose an overlay
| File | Purpose |
| --- | --- |
| `compose.yml` | Source-build base services and their dependencies. |
| `compose.dev.yml` | Source mounts and development commands. |
| `compose.docs.yml` | The documentation site and proxy routing. |
| `compose.web.yml` | The marketing site and proxy routing. |
| `compose.test.yml` | Platform container test configuration. |
| `compose.docs.test.yml` | Documentation container test configuration. |
| `compose.web.test.yml` | Marketing-site container test configuration. |
| `compose.test.mock.yml` | Mock-backed integration configuration. |
Read the script that invokes a test overlay before running it directly; the test harness may prepare images, ports and fixtures. [Docker contributions](https://docs.tale.dev/develop/contributing-docker) covers the relevant checks.
## Inspect what Compose will merge
Files apply from left to right. Later files override or extend earlier configuration according to Compose’s merge rules. Inspect service names without printing the resolved environment and its secrets:
```bash
docker compose -f compose.yml -f compose.dev.yml -f compose.docs.yml config --services
```
This inspects the static files. The `docker:dev` command also supplies its generated environment overlay. A complete `docker compose config` can print interpolated credentials; keep that output out of public logs and bug reports.
## Understand the main services
The source stack separates `backend-api` from `backend-worker`. The API handles application requests and authentication; the worker runs jobs, model turns and knowledge processing. `platform` serves the web app. `proxy` routes traffic, while `db`, `knowledge-db` and `object-store` hold application, knowledge and blob data.
`sandbox`, `sandbox-egress` and `sandbox-llm-gateway` provide isolated execution and its network/model paths. The source stack also includes the video-ingestion sidecar. Production topology can differ: the generated single-host stack combines the application and knowledge databases. Use [Container architecture](https://docs.tale.dev/self-hosted/operate/container-architecture) for service responsibilities and [Environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference) for their configuration.
## Diagnose the first failure
If preparation fails, read the wrapper’s first error before retrying Compose. Check Docker availability and image builds for an image error; network creation for an absent sandbox network; occupied ports or existing containers for a second-checkout conflict. Inspect `docker compose ps` and the failing service’s logs before changing configuration. Avoid removing volumes to solve a startup error: that discards the state needed to reproduce it.
# Connectors
Source: https://docs.tale.dev/develop/connectors
A connector gives Tale a reusable way to call a service. Its definition describes authentication, permitted destinations, and actions; each organization supplies its own credentials. Use this page to inspect that contract or contribute a new connector.
If your goal is to connect an account in the app, use [Connector credentials](https://docs.tale.dev/platform/admin/connectors). To choose an existing integration, browse the [connector catalog](https://docs.tale.dev/platform/connectors/overview).
## How a connector is declared
Definitions live in `configs/platform/system/connectors//connector.yml`, alongside the connector's icon. The directory slug must match `name`. An automation calls an action using `.`, for example `tavily.search`. Vendor connectors appear in Settings; internal connectors with platform authentication do not.
This is the identity and authentication excerpt from the shipped Tavily definition. It is not a complete connector: the file also needs its action definitions.
```yaml
name: tavily
displayName: Tavily
description: Real-time web search and page extraction for AI research.
tags:
- Search
allowedHosts:
- api.tavily.com
auth:
- method: api-key
```
### Set the destination boundary
| Field | Meaning |
| --- | --- |
| `endpointMode: fixed` | Default. Live HTTP calls use fixed vendor URLs; `allowedHosts` contains exact hosts |
| `endpointMode: per-credential` | Each credential supplies an HTTPS `endpointUrl`; actions read its origin as `ctx.endpoint`, without a trailing slash |
| `allowedHosts` in per-credential mode | Host suffixes: `atlassian.net` allows its subdomains |
| `configFields` | Non-secret per-credential values such as server host, port, region, or API version |
Confluence, GlitchTip and Shopify use per-credential origins. Keep secrets out of `configFields`; use the encrypted credential payload. For JavaScript actions, `ctx.http` enforces the declared HTTP destination boundary. Native backends, such as mailbox protocols, implement their own transport checks; an HTTP allowlist alone does not describe their whole security boundary.
Adding a connector is a source contribution. The runtime reads the platform catalog, with no organization-level connector upload. Start from [Contributor setup](https://docs.tale.dev/develop/contributor-setup), then inspect a shipped connector with the same authentication and transport before adding yours.
## What an action declares
| Field | Contract for the author and caller |
| --- | --- |
| `name`, `description` | Stable snake_case action name and an explanation of when to use it |
| `input` | Object JSON Schema, validated before execution; describe fields and mark required ones |
| `output` | TypeScript-style signature describing the result; this is documentation, not a runtime output validator |
| `effects` | `read` or `write`; writes pass through approval policy |
| `mock` | Required deterministic JavaScript implementation; same input, same output, no network I/O |
| `backend` | Optional live implementation: `yaml-js` with `live`, or `native` with an `impl` identifier |
| `exampleInput` | Optional small, meaningful example for discovery and testing |
A connector with no live backend can run in mock mode but refuses live execution. Write actions do not run if the platform cannot obtain an approval decision. See the [approval-policy reference](https://docs.tale.dev/self-hosted/configuration/approvals) for rule precedence and pending decisions.
When inspecting a result contract, read the live implementation too. For example, Tavily's search input describes `max_results`, but the shipped action caps the returned results at five. The output signature alone does not explain that bound.
### Resolve the right account
Credentials are selected at call time: the one named by the caller, otherwise the connector's default. Changing a default can therefore change which account a later run uses. Name the credential explicitly when the account is part of your integration's contract.
Mailbox discovery has a deliberate exception: `conversation.sync_mailbox` and `conversation.list_mailbox_messages` enumerate every active credential for the connector. They cover all connected mailboxes instead of limiting themselves to the default.
## The authentication methods
A connector can support several methods; a stored credential uses exactly one.
| Method | UI label | What the credential holds |
| --------- | ------------------- | ------------------------------------------------------------------------------------------------ |
| `api-key` | API key | A single secret the action body places itself — a vendor header, a query param, or a body field. |
| `bearer` | Token | A token sent as the Authorization header, under the scheme the connector names. |
| `basic` | Username & password | A username and password sent as HTTP Basic, which is also the shape a mailbox login takes. |
| `oauth2` | OAuth | An authorization-code grant: access token, refresh token, expiry, and the granted scopes. |
The internal `platform` method has no stored credential and cannot be combined with vendor methods. It is reserved for native platform capabilities such as task and document actions.
Credential secrets are encrypted at rest. Lists return a masked preview and metadata rather than plaintext; the authorized live runtime resolves the secret when it performs the action. A successful credential save proves storage, not that the vendor accepts the credential or grants the required scopes.
## Registering an OAuth app
The connector declares authorization/token URLs and requested scopes. Configure the vendor application separately, then connect an account using that application.
| Source | Precedence and configuration |
| --- | --- |
| Organization app | Takes precedence. An administrator supplies client ID and secret in **Settings > Connectors > OAuth apps** |
| Deployment app | Default when no organization app is configured: `CONNECTOR_OAUTH__CLIENT_ID` and `CONNECTOR_OAUTH__CLIENT_SECRET` |
In environment variable names, uppercase the slug and replace dashes with underscores. For a single-tenant Microsoft app, also configure its directory ID so authorization uses that tenant rather than `/common`. Organization app secrets are encrypted and not revealed again.
### Register the callback exactly
All organization OAuth connectors use this redirect URI:
```text
${SITE_URL}${BASE_PATH}/api/connectors/oauth2/callback
```
Match the scheme, host, path, and absence of a trailing slash exactly. Tale refuses to begin consent when `SITE_URL` is missing; it does not infer a public callback from the incoming request. A `redirect_uri` refusal on the vendor screen usually means the registered URI differs from the one Tale sent.
Personal OneDrive/Google Drive knowledge imports are a separate flow. Google Drive shares its OAuth application between connector and import, so register both redirect URIs on that Google client. Find the import callback in the [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference).
### Configure Slack's event endpoint
Slack's app is deployment-only: `CONNECTOR_OAUTH_SLACK_*` and `CONNECTOR_SLACK_SIGNING_SECRET`. Its incoming event must be verified before Tale knows the organization, so an organization app cannot supply this secret.
Register `${SITE_URL}${BASE_PATH}/api/connectors/slack/events` as the Events Request URL. Without the signing secret, even the registration handshake returns `503`. With valid configuration, Tale verifies signatures and identifies the organization from the Slack workspace. The endpoint currently acknowledges events; it does not turn inbound Slack messages into conversations or automatically run an automation.
## Choosing a surface
| Need | Use |
| --- | --- |
| A supported vendor action | A shipped connector and an organization credential |
| A reusable action missing from the catalog | A source contribution with schema, deterministic mock, live backend, and tests |
| Project-specific calls to your own service | A project agent's **Secrets** and sandbox code, within that sandbox's network permissions |
| Custom logic inside an automation | A `transform` node, within the runner's available capabilities and network rules |
A secret provides authentication; it does not make an unreachable private service reachable. Confirm network access from the actual sandbox or runner before designing an integration around it.
External MCP-server registration is not available. Tale's [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) lets an external client call Tale; it does not add an outbound connector to another MCP server.
## Where this fits
Test three things separately when contributing: schema validation, deterministic mock behavior, and the live vendor path. Check failure handling as well as success: missing credential, wrong scope, denied destination, invalid input, vendor refusal, and an approval wait for writes. The [contributor guide](https://docs.tale.dev/develop/contributor-setup) explains the local source environment; the [credential guide](https://docs.tale.dev/platform/admin/connectors) explains the administrator's setup.
# Build and maintain Tale images
Source: https://docs.tale.dev/develop/contributing-docker
Build an image when a change affects container dependencies, startup behavior or packaged application code. Start from a source checkout with the prerequisites in [Contributor setup](https://docs.tale.dev/develop/contributor-setup), a running Docker daemon and access to the base-image and package registries used by the Dockerfile.
A successful build proves that an image can be assembled. Run it in a separate development stack and exercise the changed behavior before using it in a deployment.
## What the images are
Dockerfiles use the repository root as their build context. Open the relevant Dockerfile for exact base-image versions and build arguments; the table describes each image’s purpose.
| Image | Dockerfile directory | Contents and role |
| --- | --- | --- |
| `tale-platform` | `services/platform/` | Debian-based application image with the built web app and native backend. Build stages use Bun and Node; API and worker roles reuse this image. |
| `tale-db` | `services/db/` | PostgreSQL with the supplied search/vector extensions, based on ParadeDB. Both application and knowledge database services use it. |
| `tale-proxy` | `services/proxy/` | Caddy configuration and proxy startup. |
| `tale-sandbox` | `services/sandbox/` | Sandbox orchestration, including Bun and the Docker CLI. |
| `tale-sandbox-runtime` | `services/sandbox-runtime/` | Python-based execution environment with coding harnesses, Node, Bun, browsers and document tools. |
| `tale-sandbox-egress` | `services/sandbox-egress/` | Alpine-based outbound proxy and DNS support. |
| `tale-sandbox-buildkitd` | `services/sandbox-buildkitd/` | BuildKit with the sandbox’s networking and startup configuration. |
| `tale-sandbox-llm-gateway` | `services/sandbox-llm-gateway/` | Model gateway built from Bifrost. |
Object storage and the video-ingestion sidecar use upstream images directly. They are not built from a Tale Dockerfile. Sandbox runtime and BuildKit images are launched on demand; do not assume that building only the services in a Compose file also builds them.
## Building locally
For an isolated proxy-image build, run this from the repository root:
```bash
docker build -f services/proxy/Dockerfile -t tale-proxy:docs-review .
```
The local `docs-review` tag makes the result distinguishable from a published release. The build must finish successfully before you test or tag it for distribution.
For services declared with `build:` in the repository Compose file, use:
```bash
docker compose build platform
```
`docker compose build` without a service selects all services that have a build definition. Build time depends on cached layers, network access, platform and the image; a browser or application image has different requirements from the proxy.
The repository Compose file defaults `PULL_POLICY` to `build`. Generated production Compose normally pulls release images. Use [Compose files](https://docs.tale.dev/develop/compose-files) for the supported development launcher, which also prepares services and images that a bare build does not start.
## The customisation seams
Choose the narrowest change that satisfies the requirement:
| Requirement | Where to start |
| --- | --- |
| Routing, TLS or public headers | `services/proxy/Caddyfile` and proxy configuration. Test callbacks and streaming after changes. |
| Application UI, backend or extraction behavior | Application source under `services/platform/`, followed by an application-image build and relevant tests. |
| A package or browser in agent sessions | `services/sandbox-runtime/Dockerfile`. Test in a newly created session using the changed image. |
| Sandbox outbound policy | Existing environment configuration first; proxy templates and startup code only when the configuration cannot express the change. |
| BuildKit behavior | `services/sandbox-buildkitd/`, including its network assumptions. |
Entrypoints, health checks and internal paths are implementation contracts. A fork must maintain and test changes to them across upgrades. A configuration-only change may not need a new image; see [Run Compose yourself](https://docs.tale.dev/self-hosted/install/own-compose).
## Tagging and pushing your own registry
After testing the image, set your registry namespace and an immutable tag. This example publishes only the proxy image built above; it is not a complete deployment image set. Registry authentication and permission to push are prerequisites.
```bash
export REGISTRY=registry.internal.example.com/tale
export IMAGE_TAG=reviewed-build-1
docker tag tale-proxy:docs-review "$REGISTRY/tale-proxy:$IMAGE_TAG"
docker push "$REGISTRY/tale-proxy:$IMAGE_TAG"
```
Record the resulting digest, source commit and build platform. Distribute all images required by the destination, including sandbox images and upstream dependencies. An offline environment also needs a plan for packages, browser downloads and model access; moving a single image does not make the system self-contained.
The CLI reads `GHCR_REGISTRY` for the Tale image namespace. Its selected version still determines the image tag, so publish the names and version tags the deployment expects. For independently pinned images and source commits, follow the [managed deployment reference](https://docs.tale.dev/self-hosted/install/cli-install#managed-deployments).
## Staying in sync with upstream
Keep the fork’s source changes under version control and review upstream changes before rebuilding. Record base-image versions or digests with the build. Review updates to those pins deliberately; rebuilding against an unchanged pin does not incorporate a newer upstream image automatically.
Before rollout, verify the startup role, health checks, public routes and the feature you changed. For sandbox changes, include a new session and its required network calls. Contribute generally useful fixes upstream when possible to reduce the code your fork must maintain.
## Diagnose a failed build or startup
| Symptom | Next check |
| --- | --- |
| A `COPY` source is missing | Run from the repository root with the expected build context; check `.dockerignore` and the source path. |
| Package or base-image download fails | Check registry access, authentication and the first failed build step. |
| The image builds but exits | Read that container’s startup logs and confirm its environment, mounts and role. |
| A sandbox still uses old packages | Confirm the configured runtime image and create a new session. An existing container is not replaced by retagging an image. |
Use [Container architecture](https://docs.tale.dev/self-hosted/operate/container-architecture) to trace service dependencies and [Upgrades](https://docs.tale.dev/self-hosted/operate/upgrades) for rollout and recovery.
# Run Tale from source
Source: https://docs.tale.dev/develop/contributor-setup
Run Tale from source when you want to change the product or test a contribution. You will run the web app and backend on your machine, with databases and sandbox services in Docker. For a packaged installation, follow the [self-hosted quickstart](https://docs.tale.dev/self-hosted/install/quickstart).
## Prepare your machine
Use a local checkout of the [Tale repository](https://github.com/tale-project/tale). Run the commands below from its root.
| Requirement | What it runs | Check |
| --- | --- | --- |
| Bun version pinned in the root `package.json` | Workspaces, dependency installation, Vite and development scripts | `bun --version` |
| Node.js 22.21.1 or newer in the 22.x line | The application backend; the container pins 22.21.1 | `node --version` |
| Docker with Compose | Application and knowledge databases, object storage and sandbox services | `docker info` and `docker compose version` |
| Free local ports | App on 3000 and backend on 3005 | `bun run setup:check` |
The repository pins its package-manager version. Match that version when reproducing a failure or contributing a lockfile change; the startup pre-flight checks only a minimum version.
The pre-flight command checks Bun and the two ports. Check Node and Docker separately; a green pre-flight result does not verify them. The first boot also needs network access to fetch dependencies and container images. A model provider is needed for real AI replies, but not for signing in and inspecting the app.
## Install and start
Install the workspace dependencies, check the local ports, then start the development stack:
```bash
bun install
bun run setup:check
bun run dev
```
The root development script creates missing secrets in the gitignored root `.env` and keeps existing values. Keep that file private and retain it between restarts: the backend and sandbox must share the same secrets.
The orchestrator starts Docker dependencies, starts the Node backend, waits for its API and authentication routes, then starts Vite. If the sandbox runtime image is missing, for example on a first run or after you removed local images, the orchestrator builds it from source before the backend starts. That single step can take several minutes, and agent sessions and code execution stay unavailable until it finishes. The backend applies database migrations during startup. Wait for the `READY` banner before opening `http://localhost:3000`; image downloads and first-time provisioning can make a cold boot slower.
Open the app and sign in. Loading the dashboard verifies the browser-to-backend path; send a message with a configured provider to check an actual model turn.
Stop the foreground processes with `Ctrl-C`. Docker data volumes persist; stopping development does not erase the instance.
## Sign in to the local workspace
The local development seeder creates `dev@tale.test` with password `TaleDev!Passw0rd` and a **Dev Workspace** organization. It leaves an existing account unchanged. The seed is restricted to loopback `SITE_URL` values.
Set `TALE_DEV_SEED_USER=0` to test first-time setup instead. To use a different local identity, supply `TALE_DEV_SEED_USER_EMAIL` and `TALE_DEV_SEED_USER_PASSWORD` through the environment. Changing these values does not reset an existing account’s password.
## Choose what to run
For normal product work, keep `bun run dev`. It starts the backend and app together and supplies the shared configuration they need.
If the backing services already run with the correct ports and credentials, skip only their Docker startup:
```bash
TALE_DEV_SKIP_DOCKER=1 bun run dev
```
This still starts a local backend. It does not make the app independent of Postgres, object storage or the sandbox. Use [Contributor compose files](https://docs.tale.dev/develop/compose-files) when your change needs the full container build.
For frontend-only work against an existing backend, run Vite directly from `services/platform` and point it at that backend:
```bash
cd services/platform
TALE_BACKEND_URL=http://localhost:3005 bunx --bun vite --host 127.0.0.1 --port 3000
```
This command does not start services, seed accounts or run migrations. The backend must already be configured for the browser origin you use.
## Resolve startup failures
| Symptom | Check next |
| --- | --- |
| `node` is missing or a Node flag is unknown | Install the Node version above and confirm that your shell resolves it. |
| Docker cannot connect | Start Docker and check `docker info` in the same shell. |
| Port 3000 or 3005 is busy | Identify the process before stopping it; it may belong to another checkout. |
| Backend fails before Vite starts | Read the first backend error and check database connectivity and credentials. |
| Sign-in works but model calls fail | Check the provider credential, selected model and sandbox services. |
| Changes appear in the wrong app | Check the URL and which checkout owns the listening process. |
On macOS or Linux, inspect the listener with:
```bash
lsof -nP -iTCP:3000 -sTCP:LISTEN
lsof -nP -iTCP:3005 -sTCP:LISTEN
```
Stop a known development process from its original terminal. Do not kill a process solely because it holds a port.
## Keep or reset local data deliberately
Databases and uploaded files persist outside the source checkout. A second Git worktree does not automatically isolate Docker service names, ports, volumes or `.env` credentials. Before running two instances, give each its own backing services and configuration.
A reset destroys development data and can affect another checkout using the same Compose project. Inspect the project's containers and volumes, back up anything you need, and stop the stack before removing state. Configuration trees under `TALE_CONFIG_DIR` have their own lifecycle; deleting a database does not reset those files.
## Use the design system
For interface changes, start with the [design-system guides](https://ui.tale.dev). They cover `@tale/ui` for app interfaces and `@tale/marketing-ui` for marketing pages, with interactive examples, tokens, and layout patterns. Reuse a package component before adding one. The guide content is in English.
## Verify a contribution
Read the repository's `AGENTS.md` and `.agents/repo.md` before changing code. Run the relevant checks while working, then the shared gate from the repository root:
```bash
bun run check
```
The gate includes formatting, lint, types and automated tests. Its Python formatting step also uses `uvx`; install that tooling before running the full gate. Browser behavior still needs a browser check, and database changes need the real-Postgres integration check required by the repository contract.
Update affected docs and every shipped locale with your change. For container work, continue with [Build Docker images](https://docs.tale.dev/develop/contributing-docker); for external integrations, start with [Call Tale from a script](https://docs.tale.dev/tutorials/developer/call-tale-from-a-script).
# MCP endpoint
Source: https://docs.tale.dev/develop/mcp-endpoint
Connect an MCP client when an agent needs to discover Tale's tools, retrieve knowledge, or build and run automations. The connection uses the same API key and organization scope as [REST](https://docs.tale.dev/develop/api-reference). Tale is the server in this connection: your external client calls Tale.
Start with `initialize`, inspect `tools/list`, then call `get_docs` before writing an automation. The deployment supplies its own supported grammar, so the client does not need to invent node types or configuration fields.
## Connect a client
### Prepare the connection
Create an [API key](https://docs.tale.dev/platform/admin/api-keys) and keep it in your client's secret configuration. The app's **Settings > API > MCP** page shows the endpoint, organization slug, and a copyable discovery request.
| Setting | Value |
| --- | --- |
| Endpoint | `https://your-host.example.com/api/v1/mcp` |
| Transport | HTTPS POST with JSON-RPC; plain JSON responses |
| Authorization | `Authorization: Bearer ` |
| Organization | `X-Organization-Slug: ` |
| Protocol revisions | `2025-06-18`, or `2025-03-26` when proposed by the client |
Use a client that supports a remote HTTP endpoint with custom headers. There is no SSE event stream, session deletion, or OAuth authorization flow. OAuth discovery URLs return JSON `404`; a client requiring that flow needs a different authentication configuration. A client that only launches local stdio servers cannot use this URL directly. [Use Tale from your editor or a script](https://docs.tale.dev/develop/use-tale-from-your-editor) has ready configurations for opencode and Claude Code.
Always send the organization header in reusable integrations. It is optional only when the key holder has one organization. With several memberships, omitting it returns `400 ORG_SLUG_REQUIRED`; an unknown slug returns `404 ORG_SLUG_INVALID`, and a non-member organization returns `403 ORG_FORBIDDEN`. Each of these refusals lists the slugs you can send in `data.organizations`.
### Initialize and retrieve the authoring reference
The examples assume `TALE_URL`, `TALE_API_KEY`, and `TALE_ORG_SLUG` are already set in your environment. `TALE_URL` is the application origin, without `/api/v1`.
```bash
curl --fail-with-body "$TALE_URL/api/v1/mcp" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG" \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"docs-client","version":"1.0.0"}}}'
```
The response identifies the server as `tale-platform`. Read `result.protocolVersion` and send that negotiated value as `MCP-Protocol-Version` on later calls. The next example uses `2025-06-18`; replace it if your initialization negotiated the older revision. An unsupported header value returns `400`.
```bash
curl --fail-with-body "$TALE_URL/api/v1/mcp" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG" \
--header 'MCP-Protocol-Version: 2025-06-18' \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_docs","arguments":{}}}'
```
A successful `get_docs` result contains the automation reference as text and has no error flag set. To inspect tool schemas instead, send `method: "tools/list"`. The current inventory has 22 tools. Keep the JSON-RPC `id` so a client can match a result to its request.
### Transport and batches
| Request | Response behavior |
| --- | --- |
| One JSON-RPC message | One JSON-RPC result or error |
| Batch of up to 20 messages | An array of responses; notifications have no response entry |
| Notifications only | HTTP `202` |
| `OPTIONS` | HTTP `204`, `Allow: POST, OPTIONS`; no key required |
| Any other HTTP method | HTTP `405`, `Allow: POST, OPTIONS` |
Every additional tool call in a batch consumes the same request budget as a separate call. If a batch exhausts its budget, the refused entry is JSON-RPC `-32000` with `data.retryAfterMs`; the enclosing HTTP response remains `200` and has no `Retry-After`. A single request rejected at the HTTP boundary gets REST `429`. Handle both cases using the [rate-limit guidance](https://docs.tale.dev/develop/rate-limits).
The endpoint supplies no CORS headers for browser key use. Keep the API key on a trusted server or in the MCP client's credential store.
## The tools
`tools/list` is the source for each tool's input schema. Missing, mistyped, blank, or unexpected arguments receive JSON-RPC `-32602` before execution. The document passed to `validate_automation`, `run_automation`, `test_automation`, or `save_automation` is intentionally an open envelope: `get_docs` explains its grammar, and the engine validates its contents.
Tools also expose `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. Hosts can use these annotations to explain a call, but they do not grant permission or guarantee safety. Reads are marked read-only; saving writes a version; deployment and trigger changes can replace existing state; live runs can contact real services.
### Authoring
| Tool | What it does |
| --------------------- | ---------------------------------------------------------- |
| `get_docs` | The automation authoring reference — grammar, node kinds, capability nodes and the method table in this endpoint's own `tools/call` dialect — as text. |
| `get_catalog` | Every node type this deployment can execute; `kind` narrows to one node kind and `compact: true` drops the input schemas. |
| `search_catalog` | Search the node-type catalog by keyword. |
| `validate_automation` | Validate an automation document without saving it. |
| `run_automation` | Run an automation document directly against the deterministic mocks. |
| `test_automation` | Run an automation's own acceptance tests. |
| `save_automation` | Save an automation document as a new immutable version. |
| `get_automation` | Read one saved version — the latest when unversioned, `version: "deployed"` for the live one (`AUTOMATION_VERSION_UNKNOWN` while nothing is deployed). |
| `list_automations` | The organization's automations with their latest and deployed versions and the projects each is installed in (`projectIds`). |
| `deploy_automation` | Promote one saved version to be the live version. |
Use the authoring loop in this order: read the grammar and catalog, validate the document, run it against mocks, run its acceptance tests, save a version, then deploy that version. A successful mock run verifies the simulated path; it does not prove vendor credentials, network access, or real effects.
### Run & trigger management
| Tool | What it does |
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `run_deployed` | Run the deployed version live and WAIT for the finished result — output, trace and effects in one answer; a run that outlives the wait answers with its `runId` to poll. Takes the same optional `idempotencyKey` as `start_run`, on the same ledger the REST endpoint keeps: a repeat answers the first run with `duplicate: true` and starts nothing, whichever door started it. |
| `start_run` | Start the deployed version in the background and return a run handle immediately; poll get_run for the result. Takes an optional `idempotencyKey` — the REST endpoint's `Idempotency-Key`, the same ledger: the same key with the same arguments answers the first run's handle with `duplicate: true` and starts nothing, the same key with different arguments is refused (`IDEMPOTENCY_KEY_REUSED`). The `Idempotency-Key` HTTP header is refused on this endpoint (**400**, `INVALID_HEADER`) — a batch carries up to 20 calls, so the key rides in the tool arguments. |
| `list_runs` | Recent runs the key may read, newest first — of one automation or across the organization's projects; each names its `projectId`. |
| `get_run` | One run in full: status, output, trace, effects and `projectId` — a project run's id is the one `GET /api/v1/projects/{id}/runs/{runId}` takes. |
| `cancel_run` | Stop a run at its next node boundary. |
| `list_versions` | One automation's immutable version history; each row says whether it is the `deployed` one, and `deployedVersion` names it beside the list (`null` while nothing is deployed). |
| `list_triggers` | What starts the automations (never the webhook secret). |
| `delete_trigger` | Unbind an automation's trigger; its versions and run history stay. |
| `set_trigger` | Bind what starts the automation (schedule/webhook/event). A webhook's `token` is answered once, here, and never again — store it; `deployed` says whether deliveries will run: a trigger bound to an automation with no deployed version is stored and fires nothing until one is deployed. |
| Choose | When |
| --- | --- |
| `run_automation` | Try an unsaved document against deterministic mocks; `mode: "live"` is refused |
| `run_deployed` | Run the saved deployment live and wait up to 30 seconds; poll the returned `runId` if it continues |
| `start_run` | Start the saved deployment in the background and poll `get_run`; pass `idempotencyKey` to make a retry safe |
Both deployed-run tools use the durable runner with the same authorization and execution records. `start_run` accepts an optional `projectId`. A project-bound automation must run in a project where it is installed; a sole binding can be selected automatically. With no bindings, omission means organization scope. Read `projectIds` from `list_automations` and the actual `projectId` from the returned handle rather than guessing a REST polling URL.
### Capabilities & knowledge
| Tool | What it does |
| --------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `search_capabilities` | Search everything this organization can do — its deployed automations, by name and description. |
| `invoke_capability` | Invoke one capability by id. An action the organization gates returns a pending-approval result instead of running. |
| `get_knowledge` | Retrieve passages from the organization's knowledge — its documents and its crawled web pages. `corpus` is `private` (documents), `public-web` (crawled pages) or `all`; the REST spellings `documents` and `web` are taken too. `query` is capped at 2000 characters. Each passage carries `text`, `source` (a title), `ref`, `corpus`, `chunkIndex`, `score`, `similarity` when the dense leg ranked it, `url` for a web page, and — for a document — the `documentId` that `GET /api/v1/documents/{id}` takes (a project file's id for a project hit) and its `projectId`, the same citation the REST search answers. |
The capability registry currently contains deployed automations. It does not include builtin tools, connector actions, skills, or external MCP servers. Invoking a deployed automation is the same live operation as `run_deployed`. If approval is needed, a `pending` result lets the client explain that a person must decide before execution continues.
## What the key may do
| Operation | Required access |
| --- | --- |
| Reads, validation, mock runs and acceptance tests, capability search, knowledge retrieval | Organization membership, plus the resource's normal access rules |
| Save, deploy, set/delete a trigger, cancel a run, or execute live | Developer capability, plus the resource's normal access rules |
The key identifies its holder; it does not expand that person's role or project access. Live `invoke_capability` calls also pass through the execution checks.
Read `GET /api/v1/me` before configuring privileged tools: `capabilities.developer` reports the current role gate, while `deploymentEditor` is a separate operator allowlist and does not authorize MCP authoring. The MCP tool-error envelope below still applies; a REST capability check does not change JSON-RPC error handling.
### Distinguish a transport error from a refused tool
| Result | How to handle it |
| --- | --- |
| JSON-RPC `-32601` | Correct the unknown method |
| JSON-RPC `-32602` | Correct the tool name or arguments using `tools/list`; a value outside an enumerated set is refused with the set named |
| Tool result with `isError: true` | Read its text payload's stable `code`, explanatory `error`, and actionable `hint`; `data` may contain field problems |
| `validate_automation` with `valid: false` | Normal validation result; inspect `errors`, even though `isError` remains false |
| Capability result `pending` | Normal approval outcome; do not treat it as completion or retry it as a failure |
| Capability result `refused` | Error result; correct the stated cause |
Tool refusal codes include `AUTOMATION_NOT_FOUND`, `AUTOMATION_VERSION_UNKNOWN`, `AUTOMATION_NOT_DEPLOYED`, `RUN_NOT_FOUND`, `AUTOMATION_INVALID`, `AUTOMATION_TESTS_FAILING`, `LIVE_MODE_UNAVAILABLE`, and `NOT_SUPPORTED`. The latter means the host does not support that run/version/trigger operation. `start_run` refuses a reused `idempotencyKey` with different arguments as `IDEMPOTENCY_KEY_REUSED`; `invoke_capability` refuses an id the registry does not hold — a saved-only automation is not in it — as `CAPABILITY_NOT_FOUND` and input its schema rejects as `CAPABILITY_INPUT_INVALID`; `get_knowledge` lifts the knowledge endpoint's own codes through (`KNOWLEDGE_UNAVAILABLE` when the search itself failed). Platform errors retain their own code, hint, and optional data; for example, missing developer access returns `FORBIDDEN_DEVELOPER_SETTINGS`.
An unknown automation name is an error even for `list_versions`, `list_runs`, and `list_triggers`; an empty list means an existing automation has no matching items. The one exception is run history: a deleted automation keeps its runs, so `list_runs {name}` answers them for as long as they exist, and only a name that never ran is `AUTOMATION_NOT_FOUND`. `get_catalog` narrowed to a core node kind (`transform`, `llm`, `agent`, `subautomation`) answers an empty list with a `hint` pointing at `get_docs`, as `search_catalog` does. Invalid documents passed to tools that need a valid one, search failures, and missing deployments set `isError: true`. Only the validation tool reports an invalid document as its ordinary verdict.
## Where this fits
REST and MCP share keys, organization scoping, and durable run objects. Use REST when you want explicit HTTP routes; use MCP when your client understands tool discovery and calls. Tale does not register or call external MCP servers through this endpoint.
# Build with Tale
Source: https://docs.tale.dev/develop/overview
Use these guides when writing a client, connecting an external system, or changing Tale itself. Start with the smallest working request, then add the authentication, scope, and recovery handling your integration needs.
## Choose a development task
| What you want to build | Start here |
| --- | --- |
| A script that sends a message and reads the reply | [Call Tale from a script](https://docs.tale.dev/tutorials/developer/call-tale-from-a-script) |
| A client for projects, tasks, files, or other resources | [API reference](https://docs.tale.dev/develop/api-reference) |
| A connection from an MCP client | [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) |
| AI help in your editor, with Tale's knowledge or models | [Use Tale from your editor or a script](https://docs.tale.dev/develop/use-tale-from-your-editor) |
| An automation triggered by another system | [Webhooks](https://docs.tale.dev/develop/webhooks) |
| A filesystem client for documents | [WebDAV API](https://docs.tale.dev/develop/webdav-api) |
| A new connector | [Connector development](https://docs.tale.dev/develop/connectors) |
| A change to Tale's application code | [Contributor setup](https://docs.tale.dev/develop/contributor-setup) |
## Make the first request reliable
Choose the credential with the surface: REST, MCP, and the model endpoints use API keys, WebDAV uses an app password, and a webhook uses a secret trigger URL. These credentials are not interchangeable.
Create a separate API key for each integration, send it only to the intended instance, and keep it out of source control. [Make your first API request](https://docs.tale.dev/get-started/developers) explains instance URLs and organization scope. For a long-running operation, distinguish the accepted request from its eventual result: poll the resource or run and handle a failed outcome.
Read the [rate limits](https://docs.tale.dev/develop/rate-limits) before adding retries. Check [instance availability](https://docs.tale.dev/develop/status-page) when troubleshooting a connection. The API reference describes the error envelope and the generated specification for the current checkout.
## Build inside the platform
For agents, projects, and the automation editor, use the [Developer guide](https://docs.tale.dev/platform/developer/overview). [AI-assisted development](https://docs.tale.dev/develop/ai-assisted-development) explains how to combine authoring tools with validation and review.
# Handle rate limits
Source: https://docs.tale.dev/develop/rate-limits
Tale limits API traffic by the key holder. All API keys belonging to the same person share that person’s budget. Account for every integration and polling worker using that identity, rather than budgeting each key independently.
The limits below describe the current backend. An operator’s proxy or a downstream provider may impose additional limits.
## The buckets
A token bucket refills continuously up to its burst capacity. A short batch can use that capacity, but sustained traffic must stay within the refill rate.
| Traffic | Sustained rate | Burst | Budget owner |
| --- | --- | --- | --- |
| General `/api/v1` traffic, including MCP and the model endpoints | 120/minute | 200 | Key holder |
| Run starts, model-message sends and task starts | 20/minute | 40 | Key holder |
| Project upload handoff and file binding | 240/minute | 300 | Key holder |
| Failed API-key authentication | 20/minute | 40 | Source IP |
| Webhook deliveries before token validation | 120/minute | 240 | Sender address |
| Deliveries to a verified webhook trigger | 20/minute | 40 | Trigger |
REST execution and upload requests also consume the general budget. For example, a project file needs an upload-handoff request and a file-bind request; each counts against both the general and upload budgets. The larger upload bucket does not allow a user to bypass the general limit.
Execution includes project and non-project automation starts, thread-message sends and explicit task starts. Task intake also consumes the execution budget when `runWorkflowSlug` is supplied. A starting-work request is charged once its body and headers have passed the endpoint's own checks — a `400 INVALID_BODY` or `INVALID_HEADER` spends nothing — and before anything is looked up, so a `404` for a thread, task or automation you cannot see costs a token, as does a `409` the state answers. Some mutations, such as task comments and folder changes, have additional domain budgets shared with the app.
MCP batches have their own accounting: additional tool calls consume additional request budget. See [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) for the difference between an HTTP `429` and a refused message inside a batch. Webhook budgets are separate from API-key traffic; both sender and trigger limits must allow a delivery.
Calls to the [model endpoints](https://docs.tale.dev/develop/api-reference#model-endpoints) under `/api/v1/openai` and `/api/v1/anthropic` count against the general budget only, one request each, streamed or not; what a call may spend is capped separately by budget rules. A person, and each API key, may also have eight of these calls running at once: a ninth answers `429` with `code` `MODEL_API_CONCURRENCY_EXCEEDED` and `Retry-After: 2`.
The execution bucket limits how quickly messages are accepted, not how many turns run at once. Accepted chat messages share a deployment-wide queue across organizations and keys. Each worker batch runs up to `WORKER_CONCURRENCY` turns, 5 by default; the next batch waits for the current one to settle. A successful send can therefore wait behind other clients’ work. Queue position and estimated start time are not exposed.
## The 429
An HTTP limit refusal includes `Retry-After` in whole seconds. The JSON body provides the same wait in milliseconds. This illustrative response means wait at least two seconds:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json
{
"error": "Too many requests — retry after 1500 ms",
"code": "RATE_LIMITED",
"requestId": "example-request-id",
"data": {"retryAfterMs": 1500}
}
```
Branch on `code`; `error` is a sentence describing the wait, and `requestId` identifies the request for investigation. This is the REST response format. The app’s `/api/app` and webhook limit responses keep the machine code in `error`, so do not parse that text across surfaces. Tale does not expose remaining-budget counters: track your traffic and honor the server’s wait instruction.
1. Stop the worker’s immediate retry loop.
2. Wait at least `Retry-After`. If multiple workers share the identity, coordinate their pause.
3. Retry with a bounded exponential delay and jitter when refusals continue. For example, grow a delay from one second up to sixty seconds, always honoring a longer server-provided wait.
4. Preserve the original idempotency key for operations that support one. A timeout after a run start may mean the run was already accepted.
A spending cap answers `429` too, with `code` `BUDGET_EXCEEDED`: a budget rule that applies to the key holder — their own, a team’s, the organization’s, or the API key’s — has been reached. A short wait does not help. `Retry-After` names the time until the cap’s period resets, and `data` names the cap: `scope`, `period`, `limitCode`, `used`, `limit`, and `resetsAt` in epoch milliseconds. Nothing is queued; pause the work until `resetsAt`, or ask an administrator to raise the limit under [Policies & Limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits). On the model endpoints, both 429s come in the OpenAI or Anthropic error shape with the same `code` and `Retry-After`. A spent budget names its cap in the message there instead of in `data`, and carries `x-should-retry: false` so the vendors' SDKs do not retry it on their own.
Other `4xx` responses usually need a corrected request, credential or permission. Do not treat every failure as a rate limit; use the [error model](https://docs.tale.dev/develop/api-reference#error-model).
## Plan polling and retries
An `ETag` response of `304` still costs a request. It saves response bytes, not budget. Polling one run every five seconds consumes twelve reads per minute before any retries or other work. Leave capacity for those other calls instead of filling the entire budget with polls.
Request only the fields you need, such as `?fields=status,finishedAt` on a run. Slow down when a run is waiting for a human, and stop polling terminal runs. Follow [Start a run, then poll it](https://docs.tale.dev/develop/api-reference#start-a-run-then-poll-it) for states and idempotent starts.
For larger imports, use supported batch operations such as `POST /api/v1/contacts/bulk`, and spread batches over time. Creating more keys for the same user does not increase the budget. If a workflow needs its own service identity, provision that identity through your normal account and permission process; do not use key rotation as a retry strategy.
# Check instance availability
Source: https://docs.tale.dev/develop/status-page
Open `/status` on your Tale host to check availability without signing in. Monitors can read the same summary from `/status.json`. The summary covers the backend and deployment stores; it does not prove that every model or organization-specific connection works.
## Poll the status document
Set `TALE_BASE_URL` to the instance URL and request its status:
```bash
curl --fail-with-body --silent --show-error \
"$TALE_BASE_URL/status.json"
```
Inspect the JSON body, not only the HTTP status. During local verification with an unavailable object store, the endpoint returned HTTP `200` and this degraded result:
```json
{
"status": "degraded",
"checkedAt": "2026-09-14T04:31:20.847Z",
"components": [
{ "id": "backend", "status": "operational" },
{ "id": "database", "status": "operational" },
{ "id": "object-store", "status": "outage" }
]
}
```
In this case the app can answer requests and reach its database, while file operations need investigation. A monitor that treats every HTTP `200` as healthy would miss that failure.
## Interpret the components
| Field or component | Meaning |
| --- | --- |
| `status: operational` | All reported components are available. |
| `status: degraded` | Some components are unavailable. |
| `status: outage` | All reported components are unavailable. |
| `checkedAt` | Time of the status check, in UTC. |
| `backend` | Application backend reachability. |
| `database` | Application and deployment knowledge database health, combined. |
| `object-store` | Deployment file-store health. |
Component entries report `operational` or `outage`. If the backend cannot answer, the dependent store rows cannot be established and show as unavailable too. Accept new component IDs without breaking your parser.
## Configure a monitor
For an automated health check, install `jq` and make the verdict affect the command’s exit status. This example fails on a network error, invalid JSON, or any overall state other than `operational`:
```bash
set -o pipefail
curl --fail-with-body --silent --show-error --max-time 10 \
"$TALE_BASE_URL/status.json" | jq -e '.status == "operational"'
```
The JSON endpoint requires no API key and does not consume an API-key budget. Its result is cached for five seconds; backend store probes refresh separately, so it is a recent summary rather than a fresh storage transaction per poll. Set a request timeout and alert on repeated failures or a non-operational body according to your service needs.
`/status.json` allows cross-origin reads with `Access-Control-Allow-Origin: *`. `HEAD` returns headers without the body and `OPTIONS` advertises `GET, HEAD, OPTIONS`. Use `GET` when your monitor must inspect the component verdict.
## Distinguish liveness from readiness
The production web server’s `/api/health` endpoint is a lightweight process check. It is useful for container liveness, but does not replace the status document’s dependency checks. A development Vite server can proxy that path differently and return `404`; use `/status.json` to inspect the running development app.
On a production deployment, `/health` is the edge proxy’s own liveness response: it returns `OK` even while the application restarts. An unrecognized route such as `/healthz` can return the app shell with HTTP `200`. Neither is a readiness check. Use the documented `/status.json` or `/api/health` endpoint according to what your monitor needs to establish.
A green status does not check an external model’s credit, entitlement or availability. It also does not exercise a full upload, knowledge query, chat turn or automation. Add a controlled end-to-end check for the operation your integration depends on.
## Investigate a failure
For a degraded component, use [Troubleshooting](https://docs.tale.dev/self-hosted/operate/observability/troubleshooting) to find the corresponding logs. For a failed API call with healthy instance status, inspect the [API error code](https://docs.tale.dev/develop/api-reference#error-model); `429` is a [rate-limit response](https://docs.tale.dev/develop/rate-limits), not an instance-outage verdict.
Cloud instances expose the same status paths on their own host. Service incident communication and assurance materials are described in [Trust and compliance](https://docs.tale.dev/cloud/trust-and-compliance).
# Use Tale from your editor or a script
Source: https://docs.tale.dev/develop/use-tale-from-your-editor
Tale reaches your development work in four ways. They differ in which language model runs and in how much of the work Tale governs:
| You want | Use | Model | What Tale governs |
| --- | --- | --- | --- |
| Your organization's models as the model provider of opencode, Claude Code, or an SDK script | [The model endpoints](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints) | An organization model you name by its id in each request | Each model call: model access, input guardrails, budgets, usage under your name and key; not the answers |
| Ask the workspace assistant from a script | [The REST chat API](https://docs.tale.dev/develop/use-tale-from-your-editor#script-the-rest-chat-api) | An organization model you name in each request | The whole turn: model access, budgets, usage under your name |
| Tale's knowledge and automation tools inside opencode or Claude Code | [The MCP endpoint](https://docs.tale.dev/develop/use-tale-from-your-editor#connect-opencode-or-claude-code) | The model configured in your editor | Only the tool calls; the editor's model calls stay outside Tale |
| Have a script edited on your organization's models | [A project agent on a task](https://docs.tale.dev/develop/use-tale-from-your-editor#let-a-project-agent-edit-scripts) | An organization model the agent is configured with | The whole run: sandbox, budgets, usage, review |
An editor can use two of them with the same key: the model endpoints as its model provider and the MCP endpoint for Tale's knowledge. [The last section](https://docs.tale.dev/develop/use-tale-from-your-editor#where-the-openai-compatible-endpoint-went) explains how the model endpoints differ from the OpenAI-compatible layer of Tale 0.3.
## Create an API key
Every path from outside the app starts with a personal API key. Owners, Admins, and Developers create one under **Settings > API > REST** with **Create API key**, choosing a name and an expiration, and so can a member an Admin granted a competence that is used with a key, such as **Call models over the API**. The secret is shown once; copy it into your secret store or a private shell environment before closing the dialog. [API keys](https://docs.tale.dev/platform/admin/api-keys) covers creation, rotation, and revocation.
A key acts as you. It carries your current role and project access, and the usage it causes is booked under your name. Chat turns, model calls, and automation runs you start with the key also record the key, so API-key limits apply to them as well; a project-agent run is booked to you alone. An Admin can cap what you spend with a personal, team, or role budget under [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits); a request over a cap is refused with `429 BUDGET_EXCEEDED`.
The examples on this page read three environment variables. `TALE_URL` is your instance's origin without `/api/v1`. `TALE_ORG_SLUG` is the organization slug, shown under **Settings > API > MCP**, under **Settings > API > Models** once the model endpoints are on, and returned by `GET /api/v1/me`. Export all three in your shell, and keep `TALE_API_KEY` out of files you commit.
## Use your organization's models from your tools {#model-endpoints}
The model endpoints let opencode, Claude Code, and scripts built on the OpenAI or Anthropic SDKs use your organization's approved models as their model provider. Tale speaks the OpenAI Chat Completions and Anthropic Messages interfaces these tools already use, so you change a base URL, a key, and a model name rather than your code. Each request is a bare model call: no workspace assistant, thread, or Tale tool is involved, and the answer comes back as the model sent it.
### Before you start
The endpoints are off until an Admin turns on **Model endpoints for API keys** under **Settings > Governance > Models**; [Models](https://docs.tale.dev/platform/admin/governance/content-models#model-endpoints) describes the switch. Owners, Admins, and Developers may then call them through their role; any other member needs the competence **Call models over the API**, which an Admin grants under [Competences](https://docs.tale.dev/platform/admin/governance/competences). `GET /api/v1/me` answers `capabilities.modelApi: true` once your key may call them.
**Settings > API > Models** collects what your tools need: both base URLs, the organization slug, the models you can call with a copy button for each id, and configuration to copy for opencode, Claude Code, and the OpenAI SDK for Python. While your organization has not turned the endpoints on, the tab reads **Model endpoints are not enabled for your organization**, and Owners and Admins find **Open model access** there.

### Connection settings
| Setting | Value |
| --- | --- |
| OpenAI-compatible base URL | `https:///api/v1/openai`; the OpenAI SDKs and opencode append `/chat/completions` |
| Anthropic-compatible base URL | `https:///api/v1/anthropic`; the Anthropic SDKs and Claude Code append `/v1/messages` |
| Key | Your personal API key as `Authorization: Bearer `. Tale refuses the `x-api-key` header with `401`, so an Anthropic client must send the key as an auth token |
| Organization | `X-Organization-Slug: `, required when you belong to several organizations |
| Model | `/`: the provider's slug, a slash, and the model's id in that provider's catalog, such as `openrouter/anthropic/claude-sonnet-4.6` or `deepseek/deepseek-v4-flash` |
The same id works on both endpoints. Check the key and list the ids you may call, with your own host and slug in place of `tale.example.com` and `acme`:
```bash
curl https://tale.example.com/api/v1/openai/models \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: acme"
```
The answer is an OpenAI model list: `owned_by` is the provider slug, and `created` is always `0`. It looks like this, with the ids your organization offers you:
```json
{
"object": "list",
"data": [
{ "id": "openrouter/anthropic/claude-sonnet-4.6", "object": "model", "created": 0, "owned_by": "openrouter" },
{ "id": "deepseek/deepseek-v4-flash", "object": "model", "created": 0, "owned_by": "deepseek" }
]
}
```
The list holds your organization's chat models that a provider credential of type **API key** or **Environment variable** serves, narrowed by each credential's model allowlist and by your model access. Models served through a subscription credential are not listed, and neither are models of a provider whose endpoint is set per credential, such as Azure, or models whose tool calls work only through OpenAI's Responses API, such as GPT-6.1 Sol, which these endpoints do not relay. `GET /api/v1/models` gives each of these models' context window, capabilities, and prices under the same `providerSlug` and `id`.
### Point opencode at Tale
Add Tale as a provider in your global `opencode.json` or the project's. opencode reaches it through its OpenAI-compatible package and reads the key from `TALE_API_KEY`. List every model you want to choose under `models`, keyed by its id; `model` sets the default as `tale/` followed by that id.
```json
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"tale": {
"npm": "@ai-sdk/openai-compatible",
"name": "Tale",
"options": {
"baseURL": "https://tale.example.com/api/v1/openai",
"apiKey": "{env:TALE_API_KEY}",
"headers": { "X-Organization-Slug": "acme" }
},
"models": {
"openrouter/anthropic/claude-sonnet-4.6": { "name": "Claude Sonnet 4.6" }
}
}
},
"model": "tale/openrouter/anthropic/claude-sonnet-4.6"
}
```
The `provider` block can sit beside the `mcp` block from [the MCP configuration](https://docs.tale.dev/develop/use-tale-from-your-editor#opencode) in the same file. Start opencode from a shell where `TALE_API_KEY` is set.
### Point Claude Code at Tale
Claude Code reads its model provider from environment variables. Set them in the shell you start it from:
```bash
export ANTHROPIC_BASE_URL="https://tale.example.com/api/v1/anthropic"
export ANTHROPIC_AUTH_TOKEN="$TALE_API_KEY"
export ANTHROPIC_CUSTOM_HEADERS="X-Organization-Slug: acme"
export ANTHROPIC_MODEL="openrouter/anthropic/claude-sonnet-4.6"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="openrouter/anthropic/claude-haiku-4.5"
unset ANTHROPIC_API_KEY
claude
```
`ANTHROPIC_AUTH_TOKEN` sends the key as a bearer token. Leave `ANTHROPIC_API_KEY` unset: Claude Code would send it as `x-api-key`, which Tale refuses. `ANTHROPIC_DEFAULT_HAIKU_MODEL` is the model Claude Code uses for background tasks; both model variables must name ids from your list. Claude Code's WebSearch tool asks the model vendor to run the search, so Tale refuses those requests; the tools Claude Code runs on your machine work as usual.
### Call the models from a script
The OpenAI SDK for Python needs a different base URL and the organization header. This example reads the key from `TALE_API_KEY`, asks once for a whole answer, then streams a second one:
```python
import os
from openai import OpenAI
client = OpenAI(
base_url="https://tale.example.com/api/v1/openai",
api_key=os.environ["TALE_API_KEY"],
default_headers={"X-Organization-Slug": "acme"},
)
MODEL = "openrouter/anthropic/claude-sonnet-4.6"
reply = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "What does the cron expression 0 3 * * 1 mean?"}],
)
print(reply.choices[0].message.content)
stream = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "Write a commit message for a typo fix."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
```
Tools, `tool_calls`, and images work as they do with OpenAI, and the answer's `model` is the id you sent. A stream ends with `data: [DONE]`; its closing usage chunk arrives only when you pass `stream_options={"include_usage": True}`.
The Anthropic SDK for Python takes the key as an auth token. Run it with `ANTHROPIC_API_KEY` unset, because the SDK would send that variable's value as `x-api-key`, which Tale refuses:
```python
import os
import anthropic
client = anthropic.Anthropic(
base_url="https://tale.example.com/api/v1/anthropic",
auth_token=os.environ["TALE_API_KEY"],
default_headers={"X-Organization-Slug": "acme"},
)
message = client.messages.create(
model="openrouter/anthropic/claude-sonnet-4.6",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain the error ECONNRESET in one paragraph."}],
)
print(message.content[0].text)
```
### What Tale checks on every call
Each call is governed like a chat request, for the person whose key sent it:
- **The switch and your right.** The organization has turned the endpoints on, and your role or competence lets you call them.
- **The model.** It must be on your list, and the organization's model access must allow it for you at the moment of the call. The model allowlist of the provider credential applies as well.
- **What the model can read.** Images need a model with vision, and tools a model that takes them. Images travel as OpenAI `image_url` parts with a `data:` or `https:` URL, or as Anthropic `image` blocks in base64 or with an `https:` URL, also inside tool results. Documents travel as OpenAI `file` parts or Anthropic `document` blocks.
- **The input guardrails.** The organization's content safety, personal-data protection, and moderation provider read every text the request carries before it is sent on: system and developer instructions, your turns and the assistant's, tool calls, tool results and tool definitions, and documents given as text. A block refuses the request, and a mask sends the model the masked text. The model's answers, and images and documents that are not text, are not filtered. [Guardrails](https://docs.tale.dev/platform/admin/governance/guardrails#model-endpoints) has the details.
- **The budgets.** Before the call, Tale works out its worst case, the estimated prompt plus the most output it may produce at the model's catalog price. It refuses the call with `429 BUDGET_EXCEEDED` when that worst case does not fit what is left under a budget cap that applies to you, your teams, the organization, or the key, and holds it against those caps while the call runs; a lower `max_tokens` fits more. Afterwards it books the cost the model gateway measured and the reported tokens under your name and the key. A call you break off while Tale is still checking it is never sent on and is not booked. Breaking it off once it has been sent cancels its request to the provider as well, and the call is still booked: at least for its prompt and the output that had reached you, at the model's catalog price. Each call counts as one request and appears as **Direct API** in usage analytics.
- **Eight calls at a time.** You, and each of your keys, may have eight calls running at once. A ninth is refused with `429 MODEL_API_CONCURRENCY_EXCEEDED` and a `Retry-After` of two seconds.
Your tools never see a provider key. Tale relays each call through its model gateway with a key minted for that one request and that one model. Revoking your API key under **Settings > API > REST** ends its access with the next request; an answer that is already streaming finishes.
### What the endpoints do not serve
- **Tools the model vendor runs.** OpenAI's `web_search_options` and any tool whose type is not `function` or `custom`, and Anthropic's web search, web fetch, code execution, and MCP toolsets, with the `mcp_servers` and `container` fields, are refused with `400 MODEL_API_VENDOR_TOOL_UNSUPPORTED`: Tale could neither meter nor audit them. Your own tools are relayed and come back as `tool_calls` or `tool_use`, including Anthropic's client tools `bash`, `text_editor`, `computer`, and `memory`.
- **Audio input.** Requests with `input_audio` parts are refused.
- **Requests Tale cannot bill or keep in your organization.** More than eight answers per request (`n`), files stored in the vendor account (an OpenAI `file_id`, an Anthropic `file` source), a `service_tier` other than `auto` or `default` (`auto` or `standard_only` on Anthropic), audio output, and `store: true` are refused with `400 INVALID_BODY`.
- **Other routes.** Anthropic's `count_tokens` and every other Anthropic route are not served, and neither are OpenAI routes beyond chat completions and the model list, such as embeddings, Responses, or image generation.
- **Tale's own assistant and tools.** For those, use [the REST chat API](https://docs.tale.dev/develop/use-tale-from-your-editor#script-the-rest-chat-api) or [the MCP endpoint](https://docs.tale.dev/develop/use-tale-from-your-editor#connect-opencode-or-claude-code).
### Troubleshoot a refused call
Every refusal comes in the error shape of the interface you called, so your SDK reports it as an OpenAI or Anthropic error. Its `code` field carries Tale's stable code; the [API reference](https://docs.tale.dev/develop/api-reference#model-endpoints) lists them all.
| Status and code | What to do |
| --- | --- |
| `401 UNAUTHORIZED` | Send the key as `Authorization: Bearer`, never as `x-api-key`. For Claude Code, set `ANTHROPIC_AUTH_TOKEN` and unset `ANTHROPIC_API_KEY`; for the Anthropic SDKs, pass the key as `auth_token` or `authToken`. A revoked or expired key answers `401` too. |
| `400 ORG_SLUG_REQUIRED` | You belong to several organizations. Send `X-Organization-Slug`. |
| `403 MODEL_API_DISABLED` | The organization has not turned the endpoints on. Ask an Admin. |
| `403 MODEL_API_FORBIDDEN` | Your role cannot call them. Ask an Admin to grant **Call models over the API**. |
| `404 MODEL_API_MODEL_UNKNOWN` | The id is not on your list. Copy one from `GET /api/v1/openai/models` or the **Models** tab, provider slug included. |
| `403 MODEL_API_MODEL_FORBIDDEN` | Model access blocks this model for you. Choose another model or ask an Admin. |
| `400 MODEL_API_VISION_UNSUPPORTED`, `400 MODEL_API_TOOLS_UNSUPPORTED` | Choose a model that reads images or takes tools, or send the request without them. |
| `400 MODEL_API_VENDOR_TOOL_UNSUPPORTED` | Remove the tool the vendor would run, such as a web search. In Claude Code, the WebSearch tool causes it. |
| `400 MODEL_API_GUARDRAIL_BLOCKED` | A guardrail refused the system prompt or a message, and nothing reached the model. Rephrase, or ask an Admin about the rule. |
| `403 MODEL_API_GUARDRAIL_UNSUPPORTED` | The organization's personal-data protection tokenizes, which the model endpoints cannot honour. An Admin can switch it to mask or block. |
| `429 RATE_LIMITED` | You sent more than your API keys' shared request budget allows. Wait for `Retry-After`; see [Rate limits](https://docs.tale.dev/develop/rate-limits). |
| `429 BUDGET_EXCEEDED` | A budget cap is reached. `Retry-After` names the time until it resets, and the answer carries `x-should-retry: false` so the SDKs do not retry. Wait, or ask an Admin to raise the cap. |
| `503 MODEL_API_UNAVAILABLE`, `503 MODEL_API_GUARDRAIL_UNAVAILABLE` | A service the call needs is unavailable, such as the model gateway or the moderation provider. Retry later; if it persists, ask an Admin to check the provider credential or the moderation provider. |
| `MODEL_API_UPSTREAM_ERROR` | The model vendor refused the call, and the message is the vendor's. `400`, `413`, `422`, `429`, `503`, and `529` keep their status; any other refusal arrives as `502`. |
## Script the REST chat API
The REST chat API gives a script the same assistant members use in the app's chat. It is not a bare model call: every turn runs the built-in workspace assistant, which searches your organization's knowledge when the question calls for it and redirects requests for documents or other deliverables to Tasks. The API is asynchronous. A send answers `202` with the ID of the reply, and you poll until the turn settles.
The examples share one helper, which sends the key and the organization header on every request. Save it as `tale-api.sh`:
```bash
# tale-api.sh: source it from your shell or from a script
: "${TALE_URL:?Set TALE_URL to your Tale origin, without /api/v1}"
: "${TALE_API_KEY:?Set TALE_API_KEY}"
: "${TALE_ORG_SLUG:?Set TALE_ORG_SLUG}"
tale_api() {
curl --fail-with-body -sS \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $TALE_ORG_SLUG" \
-H 'Content-Type: application/json' "$@"
}
```
Load it in your shell, check the key, and list the models you can call. The examples need curl and `jq`.
```bash
source ./tale-api.sh
# Who the key acts as, and in which organization
tale_api "$TALE_URL/api/v1/me" | jq '{email: .user.email, organization: .organization.slug, role: .organization.role}'
# The models you can name: id and providerSlug
tale_api "$TALE_URL/api/v1/models" | jq -r '.models[] | "\(.id)\t\(.providerSlug)"'
```
Export `MODEL_ID` and `PROVIDER` from one line of that list. The conversation is a script, `ask-tale.sh`, saved next to the helper and run with `bash ask-tale.sh`. It creates a personal thread, sends one question, waits for the turn to settle, and prints the reply's text. `set -euo pipefail` stops it at the first failed request, so it never goes on with an empty ID.
```bash
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/tale-api.sh"
: "${MODEL_ID:?Set MODEL_ID to an id from /models}"
: "${PROVIDER:?Set PROVIDER to the providerSlug listed with it}"
THREAD_ID=$(tale_api -X POST "$TALE_URL/api/v1/threads" -d '{"title":"Script help"}' | jq -er '.id')
BODY=$(jq -n --arg model "$MODEL_ID" --arg provider "$PROVIDER" \
'{content: "What does our runbook say about rotating database passwords?", model: $model, providerSlug: $provider}')
MESSAGE_ID=$(tale_api -X POST "$TALE_URL/api/v1/threads/$THREAD_ID/messages" -d "$BODY" | jq -er '.messageId')
# Poll every three seconds, for up to ten minutes, until the turn settles
STATUS=queued
for _ in $(seq 200); do
STATUS=$(tale_api "$TALE_URL/api/v1/threads/$THREAD_ID/generation" | jq -r '.status')
[ "$STATUS" = idle ] && break
sleep 3
done
if [ "$STATUS" != idle ]; then
echo "No reply within ten minutes; stopping the turn." >&2
tale_api -X DELETE "$TALE_URL/api/v1/threads/$THREAD_ID/generation" > /dev/null
exit 1
fi
# Read the reply the send named, then check how it ended
REPLY=$(tale_api "$TALE_URL/api/v1/threads/$THREAD_ID/messages/$MESSAGE_ID")
if [ "$(jq -r '.status' <<< "$REPLY")" != complete ]; then
jq -r '"Turn \(.status): \(.errorCode // "") \(.error // "")"' <<< "$REPLY" >&2
exit 1
fi
if [ "$(jq -r '.finishReason // ""' <<< "$REPLY")" = length ]; then
echo "The reply reached its output limit and may be cut off." >&2
fi
jq -r '[.parts[] | select(.type == "text") | .text] | join("")' <<< "$REPLY"
```
While the send waits for a worker, `GET .../messages/$MESSAGE_ID` can still answer `404 MESSAGE_NOT_FOUND`, so the script reads the reply only once the thread is idle. A turn that has not settled after ten minutes is stopped with `DELETE .../generation`. Send follow-up questions to the same `THREAD_ID` to keep the conversation's context. The send is text only, and it is refused with `409 CHAT_TURN_IN_PROGRESS` while the thread's previous turn is still running.
When a script needs the matching passages rather than an answer, `POST /api/v1/knowledge/search` returns them without the assistant; see [Search a project's files](https://docs.tale.dev/develop/api-reference#search-a-projects-files). [Call Tale from a script](https://docs.tale.dev/tutorials/developer/call-tale-from-a-script) builds the same conversation in Python with error handling, and [Send a message, then poll the turn](https://docs.tale.dev/develop/api-reference#send-a-message-then-poll-the-turn) covers retries, token limits, and failures.
## Bring Tale's tools into opencode or Claude Code {#connect-opencode-or-claude-code}
Tale's [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint), `/api/v1/mcp`, gives an editor agent Tale's tools. The most useful for script work is `get_knowledge`, which retrieves passages from your organization's documents and crawled web pages. The endpoint also exposes the automation tools: validate, test, save, deploy, and run automations, and read their runs. Saving, deploying, setting or deleting a trigger, cancelling a run, and live runs require the Developer capability. The endpoint has no chat tool and no skills tool; to copy your organization's skills into a local skills folder, read them with `GET /api/v1/skills` as described in [Save and synchronize skill bundles](https://docs.tale.dev/develop/api-reference#save-and-synchronize-skill-bundles).
The language model on this path is the one configured in your editor. Tale authenticates the tool calls and applies your permissions to them, but the prompts, your code, and every passage a Tale tool returns travel to that editor's model provider. When that provider is another service, Tale's budgets, usage records, and model-access rules do not apply to its model calls; check that your organization allows its knowledge to reach that provider before you connect. When the editor uses [the model endpoints](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints) as its provider, the passages go only to models your organization approved, and each model call is governed as described there.
The endpoint authenticates with the API key in a header and has no OAuth sign-in. Turn off a client's OAuth detection where it offers the switch.
### opencode
Add a remote server to your global opencode configuration, `~/.config/opencode/opencode.json`, or to an `opencode.json` in the project. `{env:TALE_API_KEY}` makes opencode read the key from your environment, so the file never holds the secret. Replace the host and the slug with your own.
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"tale": {
"type": "remote",
"url": "https://your-host.example.com/api/v1/mcp",
"oauth": false,
"timeout": 60000,
"headers": {
"Authorization": "Bearer {env:TALE_API_KEY}",
"X-Organization-Slug": "your-org-slug"
}
}
}
}
```
`timeout` raises opencode's five-second default for MCP requests, which a knowledge search under load or a `run_deployed` call that waits up to 30 seconds can exceed. Start opencode from a shell where `TALE_API_KEY` is set. `opencode mcp list` shows whether the server is configured; opencode prefixes the tools with the server name, as in `tale_get_knowledge`.
### Claude Code
Register the endpoint as an HTTP server. The command below stores the expanded key in your private Claude Code configuration for the current project:
```bash
claude mcp add --transport http tale "$TALE_URL/api/v1/mcp" \
--header "Authorization: Bearer $TALE_API_KEY" \
--header "X-Organization-Slug: $TALE_ORG_SLUG"
```
To share the server with a team through a committed `.mcp.json`, reference the key as an environment variable so each person supplies their own:
```json
{
"mcpServers": {
"tale": {
"type": "http",
"url": "https://your-host.example.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer ${TALE_API_KEY}",
"X-Organization-Slug": "your-org-slug"
}
}
}
}
```
`claude mcp list` reports whether Claude Code can reach the server.
## Let a project agent edit scripts
To have the edit made inside Tale, in a sandbox and with a person's review, hand the script to a [project agent](https://docs.tale.dev/platform/projects/project-agents). Tale runs a coding harness such as OpenCode in a sandbox, on the model the agent is configured with. The run counts against the budgets of the member who started it and is recorded under that person; a run you start over REST is booked to you, not to the API key. The edited files come back as the task's deliverables, and the task waits in review for a person. [Choose an agent runtime](https://docs.tale.dev/platform/agents/harnesses) compares the harnesses; OpenCode runs only through Tale's model gateway, so it never receives a provider key.
Creating the agent needs edit access to the project, which starts at the Editor role; any member who can open the project can then create the task and start the agent on it. The organization needs a model the harness can use and available sandbox capacity. In the app, open the project's **Agents** tab, select **New agent**, choose OpenCode as the agent type and a model, then create a task with the script attached, assign it to the agent, and start the agent.
The same loop works from a terminal over REST, reusing `tale-api.sh` from the chat example. First check that this deployment runs OpenCode for project agents:
```bash
tale_api "$TALE_URL/api/v1/models" | jq -r '.harnesses[] | "\(.harness)\t\(.label)"'
```
Then save the script below as `hand-to-agent.sh` next to the helper and run it with `bash hand-to-agent.sh`. It reuses the project's agent named Script editor or creates it on the first run, because agent names are unique in a project regardless of case. It then files the script as a task and puts the agent to work with a comment that mentions it.
```bash
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/tale-api.sh"
: "${PROJECT_ID:?Set PROJECT_ID to a project you can edit}"
: "${MODEL_ID:?Set MODEL_ID to a model the harness can use}"
: "${PROVIDER:?Set PROVIDER to the providerSlug listed with it}"
AGENT_NAME="Script editor"
AGENT_ID=$(tale_api "$TALE_URL/api/v1/projects/$PROJECT_ID/agents" \
| jq -r --arg name "$AGENT_NAME" 'first(.agents[] | select((.name | ascii_downcase) == ($name | ascii_downcase)) | .id) // ""')
if [ -z "$AGENT_ID" ]; then
AGENT_ID=$(tale_api -X POST "$TALE_URL/api/v1/projects/$PROJECT_ID/agents" \
-d "$(jq -n --arg name "$AGENT_NAME" --arg model "$MODEL_ID" --arg provider "$PROVIDER" '{name: $name, harness: "opencode", model: $model, modelProvider: $provider, skills: [], connectors: [], instructions: "Edit the script from the task description. Return the changed script as a file and list every change in your report."}')" \
| jq -er '.agent.id')
fi
TASK_ID=$(tale_api -X POST "$TALE_URL/api/v1/projects/$PROJECT_ID/tasks" \
-d "$(jq -n --rawfile script backup.ps1 '{externalSystem: "terminal", externalId: "backup-ps1-hardening", title: "Harden backup.ps1", description: ("Add error handling and a dry-run switch to this PowerShell script:\n\n" + $script)}')" \
| jq -er '.task.id')
tale_api -X POST "$TALE_URL/api/v1/projects/$PROJECT_ID/tasks/$TASK_ID/comments" \
-d "$(jq -n --arg agent "$AGENT_ID" '{body: ("@" + $agent + " please take this task.")}')" > /dev/null
echo "TASK_ID=$TASK_ID"
```
`externalSystem` and `externalId` make the task idempotent: sending the same pair again returns the existing task. A description holds up to 20,000 characters; for a longer script, upload it to the project as described in [Upload a file in two steps](https://docs.tale.dev/develop/api-reference#upload-a-file-in-two-steps). An agent answers to its ID and to its name in lower case with spaces replaced by dots or removed, so `@script.editor` also works.
The mention assigns the task to the agent and starts a run, and the task moves to `in_progress`. A mention that cannot start a run is saved as an ordinary comment without an error, for example when the task is not yours to change, task automation is turned off, or another run already holds the task. Set `TASK_ID` to the value the script printed, check the task, and read the agent's report once the task reaches `in_review`:
```bash
tale_api "$TALE_URL/api/v1/projects/$PROJECT_ID/tasks/$TASK_ID" | jq -r '.task.status'
tale_api "$TALE_URL/api/v1/projects/$PROJECT_ID/tasks/$TASK_ID/comments?limit=20" \
| jq -r '.comments[] | select(.authorType == "agent") | .body'
```
Review the edited files under the task's deliverables in the app before you approve the task. To send the agent back with changes, post another comment that mentions it.
## What changed since Tale 0.3 {#where-the-openai-compatible-endpoint-went}
From 0.2.10 through 0.3, Tale served an OpenAI-compatible layer: `POST /api/v1/chat/completions`, `POST /api/v1/images/generations`, and an OpenAI-shaped `GET /api/v1/models`, whose `model` field could name an agent. Tale 0.4 rebuilt the platform without it. The [model endpoints](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints) take its place for model calls, with differences a client written for 0.3 has to follow:
- The base URL is `/api/v1/openai`, or `/api/v1/anthropic` for an Anthropic client, not `/api/v1`. An OpenAI SDK pointed at `/api/v1` still receives `404 NOT_FOUND` for chat completions, and `GET /api/v1/models` is Tale's own listing of models and agent harnesses, not the OpenAI shape.
- `model` names a model as `/`, never an agent. For the workspace assistant, use the REST chat API; to put an agent to work, use a project agent.
- Image generation is not served.
- The endpoints are off until an Admin turns them on, and every call passes model access, the input guardrails, and the budgets.
[Upgrade and recover a deployment](https://docs.tale.dev/self-hosted/operate/upgrades#03-04-the-openai-compatible-api-was-removed) lists the removal for operators moving from 0.3.
# WebDAV API
Source: https://docs.tale.dev/develop/webdav-api
Use WebDAV when a file client needs folders, downloads, uploads, and edit locks over HTTP. This endpoint exposes the organization's Document Hub; project files are outside its tree. For Finder, File Explorer, or another existing client, start with [Connect with WebDAV](https://docs.tale.dev/platform/connectors/webdav).
This reference is for client implementers. First verify one authenticated listing, then a small upload. A successful `207` listing proves access; a successful upload followed by the same downloaded bytes proves the complete storage path.
## URL scheme
| Path | Access | Contents |
| --- | --- | --- |
| `/dav//documents/` | Read and write | Active Document Hub documents and folders |
| `/dav//.trash/` | Read only | Trashed documents |
| `/dav//` | Read only | The two collections above |
Encode each path segment separately. The parser normalizes Unicode to NFC and trims leading and trailing whitespace. It rejects empty names, `.` and `..`, `/`, `\`, control characters, and names longer than 255 UTF-16 code units. This is a character-length check, not a 255-byte limit. Organization slugs match `[a-zA-Z0-9_-]{1,64}`. A `.` or `..` segment in the request line, raw or percent-encoded (`%2e%2e`, `.%2e`), is refused with `404` before routing and is never resolved against the parent folder. A raw backslash counts as a segment separator for that check (`a\..\x` reads like `a/../x`); an encoded one (`%5C`) is an ordinary, refused, name character.
Use a trailing slash for folders and none for files. Listings return canonical URLs. Follow the returned `href` when addressing an existing item; do not reconstruct it from its display name, particularly when sibling documents share a title.
## Authentication
Generate an app password in **Settings > API > WebDAV** using an account with access to developer settings. The complete password appears once. Give each client its own label so you can revoke its access independently.
| Credential field | Value |
| --- | --- |
| HTTP scheme | Basic |
| Username | Your account email; the server accepts any non-empty username |
| Password | The generated WebDAV app password |
| Organization | The slug in the URL, checked against current membership |
The app password identifies the user. Neither an account password nor a REST API key is accepted. A valid password does not bypass organization membership: losing membership causes `403`. `OPTIONS` alone is available without authentication.
### Verify a listing
Set your deployment URL and email below. Each `curl --user` command prompts for the app password, keeping it out of the command itself and shell history.
```bash
export TALE_DAV_URL="https://your-host.example.com/dav/acme/documents"
export TALE_DAV_USER="you@example.com"
curl --user "$TALE_DAV_USER" --request PROPFIND \
--header 'Depth: 1' "$TALE_DAV_URL/"
```
Expect `207 Multi-Status` with XML containing the collection and its immediate children. An empty folder still has a response for the collection itself. Do not parse the XML as JSON or treat every status inside a `207` as success.
### Verify a write and download
Choose a new folder name to avoid overwriting existing work. These commands create one folder, upload a small text file, and download it:
```bash
curl --user "$TALE_DAV_USER" --request MKCOL "$TALE_DAV_URL/Client%20test/"
printf 'Hello from WebDAV.\n' > webdav-test.txt
curl --user "$TALE_DAV_USER" --upload-file webdav-test.txt \
--header 'Content-Type: text/plain' "$TALE_DAV_URL/Client%20test/webdav-test.txt"
curl --user "$TALE_DAV_USER" "$TALE_DAV_URL/Client%20test/webdav-test.txt"
```
Expect `201` for the new folder, `201` for the new file, and the text `Hello from WebDAV.` on download. Uploading to an existing file returns `204` and replaces its content. The file also appears in the Document Hub without a separate synchronization operation.
## Methods
All methods except `OPTIONS` require the app password.
| Method | Purpose | Successful response |
| --- | --- | --- |
| `OPTIONS` | Discover capabilities and the target's allowed methods | `200`, `DAV: 1, 2`, `Allow` |
| `PROPFIND` | Read properties; use `Depth: 0` for the target or `Depth: 1` for its immediate children | `207` XML |
| `PROPPATCH` | Submit property changes; see persistence limitations below | `207`, with a status per property |
| `GET`, `HEAD` | Download a file or read its headers | `200`; conditional and range requests can change the status |
| `PUT` | Create or replace a file | `201` new, `204` replacement |
| `DELETE` | Move documents to trash; recursively trash folder contents and remove folder rows | `204` |
| `MKCOL` | Create a folder whose parent already exists | `201` |
| `MOVE` | Rename or relocate a document or folder | `201` new destination, `204` replacement |
| `COPY` | Copy a document or folder tree on the server; file copies share stored bytes | `201` new destination, `204` replacement |
| `LOCK` | Acquire or refresh a write lock | `200`, with a lock token |
| `UNLOCK` | Release a lock owned by the requesting user | `204` |
`GET` on a folder is `405`; use `PROPFIND`. An omitted `Depth` defaults to `1`; `Depth: infinity` is `403`. `MKCOL` takes an empty body. `PUT` requires `Content-Length`: use a known-size file rather than chunked transfer.
`MOVE` and `COPY` use `Destination` and honor `Overwrite: T/F` and `If`. Keep the destination on the same host and in the same organization. A missing destination parent is `409`; `Overwrite: F` onto an existing item is `412`. Moving a document is atomic; moving a folder changes its parent. Destructive operations also respect legal holds and document record restrictions.
The `Allow` header describes the target: the documents tree advertises the methods above, a trash file advertises `OPTIONS, GET, HEAD, PROPFIND`, and the trash collection and organization root advertise `OPTIONS, PROPFIND`. Capability probes on a path that cannot yet be parsed still advertise the full method set. Windows discovery also receives `MS-Author-Via: DAV` and `Microsoft-Server-WebDAV-Extensions: 1`.
## Properties
| DAV property | Meaning |
| --- | --- |
| `resourcetype` | `` for folders; empty for files |
| `displayname` | Folder name or document title |
| `getlastmodified` | RFC 1123 timestamp; source modification time, falling back to creation time |
| `creationdate` | Creation time in ISO 8601 |
| `getcontenttype` | File MIME type |
| `getcontentlength` | File size in bytes |
| `getetag` | The same validator returned by `GET` and `HEAD` |
| `supportedlock` | Exclusive write-lock support |
| `lockdiscovery` | Active lock information when available |
File-only properties do not apply to collections. An ETag is a quoted content hash when one exists, otherwise a weak validator based on size and modification time, such as `W/"42-1789373842855"`. Preserve the quotes and `W/` marker; do not substitute the document ID or infer byte equality from a weak validator. `GET` supports conditional requests and byte ranges.
Custom properties are not persisted. A `PROPPATCH` containing only dead properties reports per-property `200` for client compatibility, but a later read does not return those values. A protected live property gets `403`; dead properties in that same request get `424 Failed Dependency`. Do not use these properties to store business metadata.
## Lock semantics
Use an exclusive write lock and retain its `opaquelocktoken:` token. The advertised lock support is exclusive; although the parser accepts a shared scope, the backing store permits only one live lock at a resource. Do not build a shared-editing workflow around shared locks.
| Client action | Required behavior |
| --- | --- |
| Acquire | Send `LOCK` with an XML write-lock body and `Timeout: Second-N` |
| Write while locked | Include `If: ()` |
| Refresh | Send an empty `LOCK` body with the same `If` token |
| Release | Send `UNLOCK` with `Lock-Token: ` as the owning user |
Timeouts are clamped to 1–3600 seconds. Refresh before expiry if an edit takes longer. A missing token on a protected write gives `423`; a mismatched token or an unknown refresh token gives `412`. Locks can cover descendant paths, so a parent lock can block a write below it.
Locks are stored in Postgres and expire lazily. Expired rows do not protect a resource even before cleanup removes them. Revoking an app password deletes its locks immediately. This is also a recovery path for a client that disappeared while holding a lock; it disconnects every mount using that password.
## Status codes
| Status | Meaning and next action |
| --- | --- |
| `200`, `201`, `204` | Successful read, creation, or update; see the method table |
| `207` | Inspect each resource/property result in the XML envelope |
| `400` | Correct a malformed `Destination`, `If`, `Lock-Token`, or `Timeout` header |
| `401` | Supply a valid, unrevoked app password using Basic authentication |
| `403` | Check membership, read-only namespace, legal hold/record rules, depth, ownership, and destination scope |
| `404` | Check the returned `href`, organization slug, and resource existence |
| `405` | Check the target's `Allow`; folders cannot be downloaded or overwritten as files |
| `409` | Create the destination parent first |
| `411` | Send `Content-Length` for `PUT` |
| `412` | Re-read the resource or lock state; check `If`, `If-Match`, `If-None-Match`, and `Overwrite` |
| `413` | Reduce the file or XML body size, or review the operator's upload limit |
| `415` | Send an empty `MKCOL` body; extended MKCOL is unsupported |
| `423` | Obtain the matching lock token or wait for/release the lock |
| `502` | Check cross-host destinations and object-store connectivity |
| `503` | Release unused locks for this password and honor `Retry-After` |
| `507` | Split a folder-tree operation into smaller operations |
Do not retry every refusal automatically. A missing parent or an invalid credential needs a correction; a lock conflict needs coordination with the other editor.
## Compliance
The endpoint advertises `DAV: 1, 2`. Treat the methods and limitations on this page as the implementation contract; the advertisement is not a promise that every optional WebDAV feature works. In particular, dead properties do not persist and shared editing locks are not available. Calendar, contact, search, and ACL extensions are not provided.
For wire syntax, consult [RFC 4918](https://www.rfc-editor.org/rfc/rfc4918). DAV compliance class 3 is a revision-compliance category, not a name for calendar or contact extensions.
## Limits
| Boundary | Limit or behavior |
| --- | --- |
| Recursive listing | `Depth: infinity` refused; walk one level at a time |
| Lock duration | 1–3600 seconds |
| Active locks | 200 per app password |
| Upload size | 5 GB by default; `WEBDAV_MAX_PUT_BYTES` sets the byte cap |
| XML bodies | 64 KiB for `PROPFIND`, `PROPPATCH`, `MKCOL`, and `LOCK` |
| Password creation | Up to 50 active app passwords per user in an organization |
| Usage timestamp | Updated at most once per minute per password |
Uploads stream to the object store with backpressure. The server needs the length before it can create the upload request; chunked uploads receive `411`. Folder operations have bounded traversal budgets and can return `507`; splitting a large tree is preferable to repeatedly submitting the same oversized operation.
## Network requirements
The backend serves `/dav/*`; the platform proxy exposes it on the same public host as Tale. In local development, Vite forwards `/dav` from port 3000 to the backend, so clients can use the normal local application origin. There is no separate WebDAV service to deploy.
A listing can succeed while a download or upload fails: listings require the database, whereas file bytes also require a working object store. Test both paths after changing proxy or storage configuration. Keep the proxy's body cap consistent with `WEBDAV_MAX_PUT_BYTES`.
## Security
Use HTTPS for remote mounts. Basic authentication sends the app password on every request; Base64 is encoding, not encryption. Plain HTTP is suitable only for a controlled localhost test. Store credentials in the client's password prompt or operating-system keychain, never in a URL such as `https://user:password@host/`.
The backend stores HMAC-SHA256 hashes and a four-character lookup prefix, then verifies the hash in constant time. `WEBDAV_APP_PASSWORD_HMAC_KEY` is derived from `INSTANCE_SECRET` by the platform startup configuration unless explicitly set. Keep these deployment secrets stable and backed up; changing the HMAC key invalidates existing passwords.
The password list exposes its label, prefix, creation time, and last-use time. Use those to identify and revoke a lost device's credential. Last use is throttled metadata, not a complete per-request audit trail.
## Where this fits
Use [REST](https://docs.tale.dev/develop/api-reference) for project-scoped imports, explicit IDs, and search. Use WebDAV for Document Hub file clients that expect paths and locks. Both work with Tale documents, but WebDAV does not expose the project's file tree or every REST operation.
# Webhooks
Source: https://docs.tale.dev/develop/webhooks
A webhook lets an external system start a deployed automation by posting to a secret URL. It suits services that can send an order event, form submission, or other notification to a fixed destination. A `202` response confirms acceptance and gives you a run ID; it does not confirm the automation has finished.
For a guided first setup, follow [Trigger an automation via webhook](https://docs.tale.dev/tutorials/developer/trigger-automation-via-webhook). This reference covers the delivery contract, scope, token lifecycle, and retry behavior.
## A worked trigger
### Prepare a deployed automation
Save and deploy an automation whose tests pass. Bind a webhook in the editor, or send `PUT /api/v1/automations/{name}/triggers` with `{"kind":"webhook"}` using an authorized API key. Copy the newly issued token immediately; it is returned once.
Choose the URL from the automation's scope:
| Work to start | URL | Requirement |
| --- | --- | --- |
| Project run | `/api/projects/{id}/automations/webhook/{token}` | Active project in the token's organization, with this automation installed |
| Organization run | `/api/automations/webhook/{token}` | Automation with no project bindings |
A project-bound automation refuses the global URL with `409 AUTOMATION_PROJECT_SCOPE_REQUIRED`. Do not add a `projectId` query parameter: both URLs reject it with `400 INVALID_QUERY`. A field inside the vendor body is input data, not a way to choose the project.
### Send one delivery
Store the complete secret URL in `TALE_WEBHOOK_URL` using your sender's secret configuration. The following request uses a stable ID for one logical event:
```bash
curl --fail-with-body --request POST "$TALE_WEBHOOK_URL" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-12345-paid' \
--data '{"orderId":"12345","status":"paid"}'
```
The accepted response has the shape `{"runId":"..."}` with HTTP `202`. Save that ID alongside the sender's delivery ID. Tale passes the body to the automation inside this wrapper:
```json
{
"trigger": "webhook",
"payload": { "orderId": "12345", "status": "paid" }
}
```
Read the order as `input.payload.orderId`. If the automation declares an `inputs` schema, it must describe this wrapper. A body that is not JSON becomes text in `payload`. The body limit is 256 KiB (262,144 bytes), measured as bytes arrive; larger requests receive `413`.
### Follow the result
| Delivery scope | Authenticated polling route |
| --- | --- |
| Project | `GET /api/v1/projects/{id}/runs/{runId}` |
| Organization | `GET /api/v1/runs/{runId}` |
Poll with an API key whose holder can read that scope, or open the run in Tale. The webhook token starts deliveries; it is not a credential for reading REST results. Wait for a terminal run status before reporting that the work succeeded.
### Interpret delivery responses
| Status | Code/result | Recovery |
| --- | --- | --- |
| `202` | `runId` | Accepted; follow the run |
| `202` | `runId`, `duplicate: true` | Already accepted; follow the original run, no new run was created |
| `400` | `INVALID_QUERY` | Remove `projectId` from the query string |
| `400` | `AUTOMATION_INPUT_INVALID` | Correct the wrapper/schema mismatch using `data.issues` |
| `403` | `AUTOMATION_PROJECT_FORBIDDEN` | Check that the project is active, in the right organization, and has the automation installed |
| `404` | Unknown, disabled, or mistyped token; also any non-POST method | Check the saved URL and trigger state |
| `409` | `AUTOMATION_NOT_DEPLOYED` | Deploy a version whose tests pass |
| `409` | `AUTOMATION_PROJECT_SCOPE_REQUIRED` | Use the installed project's URL |
| `409` | `AUTOMATION_DELIVERY_SCOPE_MISMATCH` | Check the scope used for the original delivery ID |
| `413` | Body too large | Reduce the payload below 256 KiB |
| `429` | Sender or trigger budget exhausted | Wait at least `Retry-After` |
A refused input creates no run. Project refusals deliberately do not distinguish a missing project, archived project, or missing installation, and do not reveal the automation's name. `GET`, `HEAD`, and `OPTIONS` all receive the same `404` as an invalid token, without an `Allow` header.
## The token is the credential
The secret in the URL authorizes delivery. This endpoint does not verify a vendor HMAC signature or use an `Authorization` header. Keep the URL out of public issue reports, shared logs, and screenshots. Tale stores its hash and uses a constant-time comparison; plaintext is disclosed only when minted.
| Change | Token effect |
| --- | --- |
| `PUT` webhook with `rotateToken: true` | New token returned once; old URL stops working immediately |
| Delete/unbind the trigger | Token revoked; saved versions and run history remain |
| Replace webhook with schedule/event | Token revoked; response includes `revoked: "webhook"` |
| Bind webhook again after replacement | New token; the original does not return |
| Set `enabled: false` | URL suspended with `404`, but token retained |
| Re-enable, including a later `PUT` that omits `enabled` | The same suspended URL becomes active again |
After a leak, rotate or unbind the trigger. Disabling it is temporary suspension, not permanent revocation. Coordinate a token rotation with the sender and replace its stored URL before resuming deliveries.
Read `revoked` in scripted trigger changes so replacing a trigger type does not silently disconnect a partner that still uses the old URL.
## Idempotency and retries
Deduplication applies to the trigger and the project in its URL. The same delivery ID can start one run in each installed project. Current project activity and installation are checked before Tale returns a cached duplicate.
| Identity | Duplicate window | What must stay the same |
| --- | --- | --- |
| Delivery-ID header | 24 hours | ID value and scope; the body may differ and still counts as the same delivery |
| No delivery-ID header | 2 minutes | Byte-identical body and URL |
For header-based identity, the first present header in this priority order wins:
```text
Idempotency-Key
X-Idempotency-Key
webhook-id
X-GitHub-Delivery
X-Gitlab-Event-UUID
X-Shopify-Webhook-Id
Linear-Delivery
X-Atlassian-Webhook-Identifier
X-Request-UUID
I-Twilio-Idempotency-Token
X-Webhook-Id
```
Header names are alternatives, not separate namespaces. Forwarding a vendor's ID as `Idempotency-Key` preserves its identity. With no ID, JSON formatting changes the body bytes and may produce a new delivery. Prefer an explicit, stable event ID whenever your sender supports one.
Repeating the example request within 24 hours returns the original `runId` with `duplicate: true`. It does not rerun a failed automation. Decide separately how to recover a failed run instead of changing delivery IDs blindly.
Retry network failures and temporary `5xx` responses with bounded exponential backoff. For `429`, honor `Retry-After`. Keep the ID unchanged if a response may have been lost. Correct other `4xx` causes before retrying. Deduplication prevents extra runs within its window; it does not guarantee exactly-once effects in an external service.
## Budgets
| Budget | Refill | Burst | Charged when |
| --- | --- | --- | --- |
| Sender IP, as reported by trusted proxies | 120/minute | 240 | Before token verification |
| Verified trigger | 20/minute | 40 | For delivery admission |
Either budget can cause `429` with `Retry-After` in whole seconds and the normal error envelope. A token authenticates access to the trigger, but there is no separate sender-account identity to budget. Use the [rate-limit guidance](https://docs.tale.dev/develop/rate-limits) and keep the delivery ID stable while backing off.
## Choose webhook or API key
Use a webhook when the sender supports a fixed event URL. Use an API key when your client must also discover automations, choose projects, or read results. [Triggers](https://docs.tale.dev/platform/automations/triggers) explains setup in the app; the [API reference](https://docs.tale.dev/develop/api-reference) describes authenticated starts and polling.
# Set up a workspace for your team
Source: https://docs.tale.dev/get-started/admins
A usable workspace needs an organization, a working model provider, and accounts with the right access. Follow this sequence to get the team started, then configure the controls that match the work you plan to do.
## Before you begin
Use an Owner or Admin account on the correct instance. The first-run setup creates the initial account and organization. If you already see your organization in the dashboard, continue with its settings rather than creating another one.
Have the provider credential ready in your password manager. The provider must support the model and tasks you intend to use. [AI providers](https://docs.tale.dev/platform/admin/providers) explains credentials, catalogs, and agent runtimes.
## Connect a provider and test chat
Open **Settings > AI providers**, select **Add credential**, and choose the provider. Complete the fields for its supported authentication method and save. Use a descriptive credential name so another administrator can identify its purpose.

Open **Home**, choose **New chat**, and select an available model. Send a self-contained prompt such as “Write a three-item meeting checklist.” Wait for the answer to finish. A saved credential alone does not prove that its account has access to the selected model.
If the model list is empty or the provider rejects the request, use the recovery steps in [AI providers](https://docs.tale.dev/platform/admin/providers).
## Add people with the access they need
Open **Settings > Members** and select **Add member**. For a new account, the form sets an initial password; an existing account keeps its credentials. This flow does not send an invitation email. Follow [members and roles](https://docs.tale.dev/platform/admin/members-and-roles) for the fields and secure handover of the initial credentials.

Choose the role for the job: Members use the workspace, Editors maintain shared content, Developers work on integrations and automations, and Admins manage the organization. Use the detailed permission table when the task crosses those boundaries. Teams and project sharing further determine which project work a person can access.
## Verify the team’s first workflow
Ask a teammate to sign in with their own account, send a message, and open the project they need. Check any shared source with that account too. Testing only as Owner can hide missing permissions or access that is broader than intended.
Start with one representative project and a small set of source documents. Confirm that people can find the work and that the intended accounts can access its files before importing a large library.
## Set the operating rules
Review [policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits), [audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs), and [SSO](https://docs.tale.dev/platform/admin/enterprise-sso) as needed. Assign responsibility for provider credentials, access reviews, and responding to failed jobs. Self-hosted operators also need a tested [backup and restore process](https://docs.tale.dev/self-hosted/operate/backups-and-restore).
# Make your first API request
Source: https://docs.tale.dev/get-started/developers
Start an integration by proving three things: the key authenticates, it targets the right organization and that organization has the resources you need. This guide gets you through those checks with curl. You need a running Tale instance and permission to create API keys, normally the Developer, Admin or Owner role.
## Create a key for the integration
Open **Settings > API > REST** and select **Create API key**. Name the key for its purpose, choose an expiration and select **Create key**. Copy the value immediately; Tale displays the secret once.

Load the secret into `TALE_API_KEY` from a secret manager or private shell environment. Set `TALE_BASE_URL` to your instance, for example `https://your-host.example.com`. Do not include a trailing `/api/v1`; the commands below add that path.
## Identify the account and organization
Call `/me` without an organization header. With one membership, it returns the key’s identity and organization. With several memberships, it returns `400 ORG_SLUG_REQUIRED` and lists your choices in `data.organizations`:
```bash
curl --fail-with-body --silent --show-error "$TALE_BASE_URL/api/v1/me" \
-H "Authorization: Bearer $TALE_API_KEY"
```
Set `TALE_ORG_SLUG` to your chosen slug, then repeat `/me` with that scope. The dashboard URL contains an organization ID; do not use it as the slug. A `400` in the first request makes curl exit with code 22 while still printing the JSON body.
```bash
curl --fail-with-body --silent --show-error "$TALE_BASE_URL/api/v1/me" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $TALE_ORG_SLUG"
```
Check the successful response’s account, organization, capabilities and `key.expiresAt` before proceeding.
The key acts with its holder’s current membership and permissions. Creating several keys for one account does not create several independent roles or rate-limit budgets. Plan key replacement before expiration; `/api/v1` does not manage API keys for you.
## Find an available model
Send the organization explicitly when listing models:
```bash
curl --fail-with-body --silent --show-error "$TALE_BASE_URL/api/v1/models" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $TALE_ORG_SLUG"
```
A `200` response with a `models` array confirms this authenticated, organization-scoped request. The array may be empty; that confirms access to the endpoint, not readiness to generate a reply.
Use a model’s `id` in chat requests and its `providerSlug` when that ID is available from several providers. A model can be listed yet unavailable to the provider account because of credit or plan restrictions. Ask an admin to check provider credentials and model access if the list is empty.
## Resolve the first error
| Response | What to do |
| --- | --- |
| `401` | Check the bearer key, expiration and revocation state. |
| `400` with `ORG_SLUG_REQUIRED` | Choose a slug from `data.organizations` in this error and send `X-Organization-Slug`. |
| `404` with `ORG_SLUG_INVALID` | The header names no organization at all — a typo, or the dashboard URL's organization ID pasted as the slug. Send the slug from `data.organizations`. |
| `403` with `ORG_FORBIDDEN` | The organization exists, but the key holder is not a member of it. Pick a slug from `data.organizations`. |
| `403` | Check the permission needed for the operation. |
| `429` | Wait as directed by `Retry-After`; read [Rate limits](https://docs.tale.dev/develop/rate-limits). |
If curl reports a TLS or network error before receiving JSON, check the host and certificate. Avoid disabling certificate verification in production scripts.
## Choose the next task
| You want to… | Continue with |
| --- | --- |
| Print a completed assistant reply | [Call Tale from a script](https://docs.tale.dev/tutorials/developer/call-tale-from-a-script). |
| Start an automation from another system | [Trigger an automation via webhook](https://docs.tale.dev/tutorials/developer/trigger-automation-via-webhook). |
| Connect an MCP client | [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint). |
| Use Tale from opencode, Claude Code or a shell script | [Use Tale from your editor or a script](https://docs.tale.dev/develop/use-tale-from-your-editor). |
| Work with project files, tasks or runs | [API reference](https://docs.tale.dev/develop/api-reference). |
Use project routes under `/api/v1/projects/{id}/...` for project-scoped work. The project ID belongs in that path; the organization slug belongs in the header. Keep both explicit in your integration configuration.
# Create and test a project agent
Source: https://docs.tale.dev/get-started/editors
Create a project agent for a repeatable job, such as reviewing documents or preparing an operational report. You choose its instructions, runtime, model, and tools, then give it a task with a result you can inspect. This guide uses a launch-brief review to show that path. Until a project has an agent of its own, its tasks can go to the organization's [standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent).
## Before you begin
You need edit access to a project, a suitable model provider, and an available agent runtime with its required infrastructure. A successful ordinary chat verifies the provider’s chat path; it does not prove that an agent runtime or sandbox is ready. Ask an administrator to check [agent runtimes](https://docs.tale.dev/platform/agents/harnesses) if none is available.
Create or open a project first. For the project’s sharing and knowledge setup, follow [use projects](https://docs.tale.dev/tutorials/member/use-projects).
## Give the agent a focused job
Open the project’s **Agents** tab and select **New agent**. Name it for the job, such as “Launch reviewer”. Choose an **Agent type** and **Model** supported by your workspace. When a model has multiple provider entries, choose the intended provider too.

In **Instructions**, describe the job, the source material, the expected output, and the limits. For example:
> Review the launch brief attached to the task. List missing decisions, unclear owners, and contradictions. Quote the relevant passage for each finding. Do not change files or contact external services. If the brief is missing, ask for it.
Grant only the **Skills, connectors & tools** and **Secrets** needed for that task. Select **Create agent** to save. You can edit the brief after reviewing a result.
Create a task with a clear description and the required input files. Assign the agent, then select **Start agent**. Assignment and starting are separate actions. Watch the task’s status and activity while it runs.
If it cannot start, inspect the displayed reason before retrying. A missing provider, unavailable runtime, policy restriction, or absent input needs a different correction.
## Review the work
Read the agent’s task comment and any output files. Compare the result with the brief: did it inspect the right source, support each finding, and stay within scope? A completed run means execution finished, not that the result is correct.
Keep review and completion explicit. Use the project’s task controls to record feedback, request another pass when needed, and mark accepted work as done. [Project tasks](https://docs.tale.dev/platform/projects/tasks) explains the statuses and reviewer field.
Test a missing-input case as well as a normal task. An agent that asks for a missing brief is more useful than one that invents what the brief might say.
## Refine one thing at a time
Improve the instruction that caused a poor result, then try a comparable task. Add tools only when the job needs them, and review the [approval behavior](https://docs.tale.dev/platform/approvals/concepts) before enabling external writes. For a longer worked example, follow [your first agent end to end](https://docs.tale.dev/tutorials/editor/first-agent-end-to-end).
Once you have a task you can evaluate reliably, use the same agent for similar work. If the process needs scheduled runs, defined stages, or connector approvals, build an [automation](https://docs.tale.dev/platform/automations/concepts) around those requirements.
# Use Tale with your team
Source: https://docs.tale.dev/get-started/members
Your everyday work in Tale starts with a conversation or a project. This guide helps you find the right place for a question, a document, and work you want to share with your team.
## Know what you can access
Start with a signed-in account and a working [first chat](https://docs.tale.dev/get-started/quickstart). Your role and project access determine what you can read or change. Members can chat and work in accessible projects. Editing organization-wide knowledge requires Editor or higher; project work follows the project’s access rules. Check [members and roles](https://docs.tale.dev/platform/admin/members-and-roles) when a control is missing.
## Ask a question with enough context
Open **Home** and select **New chat**. Describe the task, provide the information needed, and say what the result should look like. For example, paste meeting notes and ask for decisions, owners, and open questions.
Read the result before using it. When the answer cites a source, open it and check that it supports the claim. A fluent answer is not evidence that the model used the correct document. [Chat effectively](https://docs.tale.dev/tutorials/member/chat-effectively) shows how to improve an answer with follow-up questions.
## Put information in the right place
| You want to… | Use… |
| --- | --- |
| Discuss a file in this conversation | A [chat attachment](https://docs.tale.dev/platform/chat/attachments) |
| Keep source material with a project | The project’s [Knowledge tab](https://docs.tale.dev/platform/projects/manage-files) |
| Make approved material available as organization knowledge | [Knowledge documents](https://docs.tale.dev/platform/knowledge/documents) |
| Maintain a short reusable article | A [knowledge entry](https://docs.tale.dev/platform/knowledge/knowledge-entries) |
Choose the intended audience before uploading. Organization knowledge and a project’s files have different access boundaries. Uploading a file and making it searchable are separate stages; wait for indexing before testing retrieval.

If you cannot add a shared source, ask someone with edit access. Include the intended audience and where the file should live.
## Work in a project
In Home, choose a project you can access under **Projects**. Use **Tasks** to see the work, **Knowledge** for project files, and **Chats** for conversations. Project chats remain personal until shared with the project.

Open a task to read its description, assignee, and discussion. You can create tasks in any project you can open and change the ones you created or that are assigned to you, for example to hand the work to one of the project's agents, or, in a project without agents of its own, to the organization's [standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent). Follow [manage project tasks](https://docs.tale.dev/platform/projects/tasks) for ordinary task work and [use projects](https://docs.tale.dev/tutorials/member/use-projects) for a complete example.
## Return to your work
Every section opens on its own first page, whatever you did there last. On a computer, **Home** is the exception: it reopens the chat you last read, and choosing it again starts a new chat. The [navigation guide](https://docs.tale.dev/platform#navigation) explains the desktop and phone controls.
Home lists your chats together with the open tasks assigned to you or waiting for your review; choose **Chats** or **Tasks** above the list to see one kind only. If the Home panel is hidden, **Show sidebar** at the start of the header brings it back. Use a new chat for a new subject and share a project conversation deliberately when teammates need it.
Your language and theme are available from **Manage account**. The [preferences guide](https://docs.tale.dev/platform/member/preferences) explains the other account settings and which features use them.
# Send your first message
Source: https://docs.tale.dev/get-started/quickstart
Start here when you have access to a Tale workspace and want to get your first answer. You will send a short prompt, read the response, and find the conversation again.
## Before you begin
You need your instance address, an account, and a workspace with an AI provider connected. Ask the person who manages your workspace for access. To install your own instance, follow the [self-hosted quickstart](https://docs.tale.dev/self-hosted/install/quickstart); for a managed instance, follow [Cloud onboarding](https://docs.tale.dev/cloud/onboarding).
An account lets you sign in. Your organization is the workspace that holds your team’s members, projects, and configuration. Your role controls which actions you can take there.
## Start a conversation
Open your instance and sign in with the method your administrator provided. If you belong to several organizations, choose the one where you want to work. Open **Home**, then choose **New chat**, the pencil at the top of the Home list.
Use the model control below the message field to inspect the available models. It may show **Auto**; you can choose a specific model when you want to control which one answers. The available choices depend on your workspace’s connected providers and access rules.

For a first test, use a self-contained request: “Write a three-item checklist for preparing a team meeting. Keep each item to one sentence.” This does not depend on uploaded documents or connected tools.
Select **Send message** or press Enter. Your message appears in the conversation, followed by the assistant’s response. A thinking indicator may appear before the answer. Wait for the response to finish before evaluating it.
Check whether it followed the requested length and format. Ask a follow-up such as “Add who should prepare each item.” The same conversation retains the context of your earlier messages.
## Find the chat again
Home lists the conversation under **Today**; choose it to reopen it. A new chat starts a separate conversation; it is useful when you change subjects. For names, history, and response controls, read [Chat basics](https://docs.tale.dev/platform/chat/basics).
Give the model the goal, the information it should use, and the format you need. “Summarize these notes as decisions and open questions” gives it a clearer task than “Help with this.”
## If you cannot get an answer
| What you see | What to do |
| --- | --- |
| You cannot sign in | Check the instance address and sign-in method with your administrator. |
| No models are available | Ask an administrator to check [AI providers](https://docs.tale.dev/platform/admin/providers) and your model access. |
| A provider or model error | Try another available model and report the displayed error to the administrator. |
| A usage limit message | Ask the administrator to review the relevant [policy](https://docs.tale.dev/platform/admin/governance/policies-and-limits). |
| An answer without your documents | This first prompt did not supply a source. Follow [chat attachments](https://docs.tale.dev/platform/chat/attachments) or [knowledge](https://docs.tale.dev/platform/knowledge/overview) to add one. |
Continue with [using Tale with your team](https://docs.tale.dev/get-started/members) or [writing effective prompts](https://docs.tale.dev/tutorials/member/chat-effectively).
# Tale documentation
Source: https://docs.tale.dev/
Find your way around Tale, give an agent its first task, or build on the platform. Start with [your first message](https://docs.tale.dev/get-started/quickstart) if you already have an account, or [install your own workspace](https://docs.tale.dev/self-hosted/install/quickstart).
## From a task to a result
1. [Describe the work](https://docs.tale.dev/platform/projects/tasks). Give the task its goal, source files, and a clear outcome.
2. [Give an agent the task](https://docs.tale.dev/get-started/editors). Choose its instructions and tools, then start the work.
3. [Review what comes back](https://docs.tale.dev/platform/projects/task-automation). Inspect the report and files, then approve or request changes.
Each guide explains the access and setup it needs. A working chat does not prove that an agent runtime or sandbox is ready.
## What would you like to do?
Configure a focused agent, give it a project task, and inspect its first result.
Find your conversations, work with sources, and join a project.
Connect a provider, invite people, and choose their access.
Make your first API request and check the response.
## Learn by doing
Follow a complete task with example inputs and checkpoints. Check the prerequisites before starting, or [browse all tutorials](https://docs.tale.dev/tutorials/overview).
Bring a task, source files, and project instructions together.
Start a delegated task and review the report and delivered files.
Review an action before the workflow carries it out.
## Find the reference you need
- [Platform guides](https://docs.tale.dev/platform) explain controls, permissions, expected behavior, and troubleshooting.
- [API reference](https://docs.tale.dev/develop/api-reference) describes the contract for integrating another system.
- [Self-hosting](https://docs.tale.dev/self-hosted) covers installation, configuration, backups, and upgrades.
- [Tale Cloud](https://docs.tale.dev/cloud) explains managed hosting and operational responsibilities.
The same product guides apply to Cloud and self-hosted instances. Screenshots show the English interface; German and French pages use the labels from their respective interfaces.
# Govern project agents
Source: https://docs.tale.dev/platform/admin/agents
Govern an agent through its project and the organization resources it uses. There is no separate organization-wide agent roster to configure: open the project from **Projects** in Home, then **Agents** to inspect or edit its workers.
## Establish who can change the agent
Anyone who can read the project can see its agents. People with project edit access may create, update or delete agents while the project is active. Archived projects remain readable. Review [members’ roles](https://docs.tale.dev/platform/admin/members-and-roles) and [team access](https://docs.tale.dev/platform/admin/teams) when the wrong person can or cannot manage a roster.
An agent belongs to one project. Neither its ID nor access to a second project lets an integration use it as that second project’s agent. Projects allow up to 50 agents, with names unique inside each project.
## Review the resources before work starts
Open the agent’s edit dialog and review the combination, rather than checking the model alone:
| Check | Why it matters | Where to resolve a problem |
| --- | --- | --- |
| Harness, model and provider | The credential must support that execution path. | [AI providers](https://docs.tale.dev/platform/admin/providers). |
| Skills and their sharing | The project’s team scope determines which bundles can be equipped. | [Skill library](https://docs.tale.dev/platform/workspace/skills) and project access. |
| Connectors and platform tools | They grant access to services and supported data operations. | [Connector credentials](https://docs.tale.dev/platform/admin/connectors) and the agent’s equipment. |
| Secrets | The running session can read the granted values. | The agent’s **Secrets** controls, available to Owners and Admins. |
| Sandbox capacity and spending | Work needs an available environment and an allowed budget. | [Sandboxes](https://docs.tale.dev/platform/admin/sandboxes) and [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits). |
For a review agent, repository read access and reporting tools may be enough. Granting a write tool authorizes its operations within its access rules; a standing instruction to ask first is not a substitute for removing an unnecessary grant.
## Handle secret changes deliberately
Only an Owner or Admin can change secret grants. An editor can update other fields while preserving the existing grants. Secret values are encrypted in organization storage and are not returned with the agent configuration, but a running agent receives the values it is granted.
Use narrowly scoped, replaceable credentials. A secret name may be shared by several agents or automation nodes, so rotating or deleting its organization value affects every future run that refers to it. Review those uses before changing it.
## Apply the same review to API clients
The public API reads and writes the same project roster and applies the key holder’s project permissions. Every operation includes a project ID. An update provides the full configuration, including secret grants to retain; omitting them requests their removal and therefore requires administrative permission.
Use the [project-agent API example](https://docs.tale.dev/develop/api-reference#manage-a-projects-agents) for integration details. After a configuration change, reopen the agent to check the saved model, equipment and grants, then give it a small task with an outcome a person can review. [Project agents](https://docs.tale.dev/platform/projects/project-agents) covers that workflow.
# API keys
Source: https://docs.tale.dev/platform/admin/api-keys
Create an API key when a script or service needs to call Tale's REST API. A key belongs to the person who created it, not to the organization whose settings page created it: it acts as that person, follows their current permissions, and works in every organization they are a member of. A REST call names the organization it addresses with the `X-Organization-Slug` header; a key whose holder belongs to one organization may omit it. Owners, Admins, and Developers manage their keys under **Settings > API > REST**, and so does a member an Admin granted a competence that is used with a key: **Call models over the API**, **Export notifications**, or **Act for another member**. Tale refuses anyone else a new key with `403 API_KEY_CREATE_FORBIDDEN`.

## Create a key
1. Select **Create API key**.
2. Enter a **Key name** that identifies the caller, such as `Billing sync` or `Document import`.
3. Choose **Expiration**: 7, 30, or 90 days, one year, or never. The form starts at 30 days.
4. Create the key and copy its secret into the caller's approved secret store before closing the confirmation.
The complete secret is shown once. The table later shows only a masked fragment, the creation date, and when the key was last used. It lists your keys, not your teammates' keys.
Anyone holding a key can act with its owner's permissions. Keep it out of source code, chat messages, screenshots, and logs. Use an account with only the access the integration needs.
## Verify the caller
Follow the authenticated request in the [API quickstart](https://docs.tale.dev/get-started/developers). Confirm the returned identity and organization before starting a write or import. After an authenticated request, check **Last used** in the key table.
A successful authentication does not guarantee permission for every resource. Project access and the key owner's current role still apply. If a request fails, use the API's error response to distinguish an expired or revoked key from missing resource permissions.
## Rotate without an outage
1. Create a replacement key before the old one expires.
2. Update the caller's secret store and restart or reload it as its configuration requires.
3. Run an authenticated request with the replacement and check that it works.
4. Revoke the old key only after every dependent caller has moved.
Tale does not automatically rotate keys. Key creation and revocation happen in this UI, not through `/api/v1`. A caller can inspect its key's name and expiry through `GET /api/v1/me` and alert the responsible person before expiration.
## Revoke a key
Open its row menu, select **Revoke key**, and confirm. Future requests with the key can no longer authenticate, calls to the model endpoints included; an answer that is already streaming finishes. Revocation cannot be undone; create a new key if you revoke the wrong one. Creating and revoking a key each leave a row in the audit log under **Settings > Governance > Logs**, in every organization you belong to.
Do not use an old **Last used** date as the only reason to revoke a key. A monthly job or a recovery process may legitimately be idle. Check the caller identified by the name first.
## Understand permissions and limits
Role changes take effect for existing keys on subsequent requests. Disabling the owner's membership removes their access; a key does not preserve the role it had when created. Losing the role or competence that let you create keys leaves the ones you hold in place: **Settings > API > REST** keeps listing them for you to revoke, but offers no new one.
Give an integration the narrowest access that works. A notification mirror, for example, does not need an Admin account: an Admin can grant an ordinary member the `tale:notifications.export` capability, which permits that export and none of the other rights of the Admin role. The grant applies only in that organization, can expire, and ends when the member is removed. Grant it under [Competences](https://docs.tale.dev/platform/admin/governance/competences), where an integration that relays people's answers and review decisions gets `tale:rest.act-as` the same way; [Delegate the export without an Admin role](https://docs.tale.dev/develop/api-reference#delegate-the-export-without-an-admin-role) covers the API side.
REST rate limits apply to the authenticated key holder. Several keys owned by the same person do not provide separate rate-limit allowances. See [Rate limits](https://docs.tale.dev/develop/rate-limits). A [budget rule](https://docs.tale.dev/platform/admin/governance/policies-and-limits) can additionally cap what requests authenticated with one key may spend: their usage counts toward the key, and a send or a model call over the cap is refused with `429 BUDGET_EXCEEDED`. Automation runs started with the key count toward it as well, alongside the personal limits of the member the key acts for. [How usage is counted](https://docs.tale.dev/platform/admin/governance/usage-attribution) has the full rule.
A key can also call the organization's models from tools such as opencode or Claude Code, through the [model endpoints](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints), once an Admin turns them on under [Models](https://docs.tale.dev/platform/admin/governance/content-models#model-endpoints). Its holder must be an Owner, Admin, or Developer, or hold **Call models over the API**. Those calls count toward the key's budget rules like its chat messages, and they appear as **Direct API** in usage analytics under the person and the key.
API keys authenticate software calling Tale. [Connector credentials](https://docs.tale.dev/platform/admin/connectors) serve the other direction: they let Tale call an external service. [Use Tale from your editor or a script](https://docs.tale.dev/develop/use-tale-from-your-editor) shows where a key goes in opencode, Claude Code, and a shell script.
# Branding
Source: https://docs.tale.dev/platform/admin/branding
Give an organization its own logo, browser-tab icons, and accent color under **Settings > Branding**. Owners and Admins can edit these settings. They apply to the organization you have open, so check the organization name before changing shared branding.

## Choose the assets
| Setting | What to prepare |
| --- | --- |
| **Logo** | A mark that stays readable at sidebar size and works on light and dark backgrounds. SVG is preferred; raster images should be at least 64 × 64 pixels. |
| **Favicon** | A small, recognizable tab icon. You can provide separate light and dark variants. |
| **Accent color** | Your brand's hex color, then a visual check in both themes. Tale derives the displayed palette for the current theme. |
The organization name supplies the text wordmark when there is no logo. Change that name under **Settings > Organization**; there is no separate application-name field on Branding.
## Upload a logo or favicon
Use the corresponding upload field and select the image. Uploading or removing an image takes effect immediately; it is not held until you select Save. The preview and organization branding refresh after the operation succeeds.
When no explicit favicon is configured, Tale can derive one from the uploaded logo. Provide an explicit icon when the full logo becomes hard to recognize at tab size. Check the tab icon and sidebar after uploading, including in the other theme.
The header's Discard action applies to pending form edits. It does not undo an image that has already been uploaded or removed.
## Change the accent color
Edit **Accent color** and inspect the preview. Select **Save** in the settings header to persist the change, or **Discard** to return to the saved value. The color field reflects the current theme, so a derived dark-theme color may differ from the stored light-theme value. The preview shows the color as it will look once saved, so in the dark theme it can differ from the value in the field. Saving a change, uploading an image or removing one each leave a row in the audit log under **Settings > Governance > Logs**.
Buttons keep your color as closely as legibility allows. Other marks drawn in the accent use your color only where it reads as text on the page. Where it does not, Tale uses a deeper shade of it, or a lighter one in the dark theme. These marks include a link, a mention, a source reference, the selected navigation item, an unread dot, the keyboard focus ring, a switch that is on, and a progress bar. A mid-tone color can therefore look slightly different on a link or a switch than on a button.
After saving, reload the page and check a selected navigation item, a button, and keyboard focus. A color that looks good as a large swatch may be hard to recognize in a small control.
## Check where branding appears
Organization branding applies inside that workspace. Switching organizations loads the destination organization's branding. Sign-in screens appear before an organization is selected and use the platform's default branding.
If you still see an old browser icon, reload the page and check the explicit favicon fields. An explicit favicon takes precedence over one derived from the logo. Use **Reset** only when you intend to remove the organization's configured branding, and read the confirmation first. Confirming takes effect immediately: the images are deleted and the cleared accent color is saved, so no further **Save** is needed and nothing is left unsaved.
# What’s new
Source: https://docs.tale.dev/platform/admin/changelog
Open **What's new** to read release notes for the Tale platform. Every signed-in member can view them; you do not need an administrator role.
## Open the release notes
Use **What's new** in the user menu, or select **View** on an upgrade notification. Opening the notes acknowledges the update and clears its unread indicator. The page shows the relevant releases, or tells you when you are up to date.
Expand a release to read its version, date, title, and notes. Focus on changes to the tasks your team uses, then open the linked product guide when you need the current instructions.
## Find older or unavailable notes
Use the link to older releases on GitHub when the change you need is outside the displayed range. The viewer obtains public release information from GitHub and caches it; a deployment with no network access may be unable to load uncached notes.
A version that has not yet been published can have no matching release entry. Check the linked release history rather than assuming that an empty view proves nothing changed.
## Distinguish releases from organization activity
Release notes describe changes to Tale itself. They do not show who changed an organization's settings or content; use [audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs) for that investigation.
If you operate a deployment, follow [the upgrade guide](https://docs.tale.dev/self-hosted/operate/upgrades) for the upgrade procedure, then review the release notes for changes your team needs to know.
# Connector credentials
Source: https://docs.tale.dev/platform/admin/connectors
Add connector credentials so Tale can use services such as a mailbox, file store, or issue tracker. Owners, Admins, and Developers manage them under **Settings > Connectors**. Choose the service and account your work needs; the [connector catalog](https://docs.tale.dev/platform/connectors/overview) explains each service's available actions.
## Add an account
1. Select **Add credential**, then the connector. Already configured connectors appear first and can hold additional credentials.
2. Check the **Name**. It starts as the connector's name, with a number added when another credential for that connector already uses it: `GitHub`, then `GitHub 2`. A name such as `Support inbox` or `EU store` is easier for an automation author to recognize. An OAuth connection has no name field: Tale names it the same way when consent completes (a second Slack workspace takes the workspace's name), and **Edit credential** renames it.
3. Complete the authentication method offered by that connector. For OAuth, select **Connect** and complete the vendor's consent flow. Each **Connect** adds a new credential for the account you authorize and never replaces an existing one, so sign in to the vendor as the account you want to add.
4. Complete the form and check the resulting row, including its connector, account or instance, and status.
Slack connects one credential per workspace. Authorizing a workspace that is already connected renews that workspace's credential instead of adding a second one.
The connector determines which fields appear. Use the account's actual credentials, not a Tale API key.
| Method | Required information |
| --- | --- |
| API key | The key issued by the service, such as Tavily or Shopify. Jev decisions takes an OpenRouter API key. |
| Token | A service token, such as a GitHub personal access token, a Discord bot token, or a GlitchTip API token with `project:read` and `event:read`. |
| Username and password | The service's expected pair. This can be a login and app password, or a vendor-specific ID and token. |
| OAuth | Authorization in the vendor's browser flow; Tale stores the returned authorization. |
Some connectors also require an instance address. For Confluence, use the Atlassian site origin. For GlitchTip, use the instance origin, such as `https://app.glitchtip.com`; a self-hosted instance must also be allowed by the deployment's connector host policy. For Shopify, use the store's `myshopify.com` origin, not the customer-facing storefront domain.
## Choose the default
The table contains one row per credential and updates while it is open: a credential another admin adds, changes or deletes appears there without a reload. The **Default** badge marks the credential used when an action does not explicitly name one. Select **Make default** in a row's menu to change it; one default is allowed per connector.
A connector with several credentials and no default can still serve callers that name a credential. Callers that omit the name need a default. Name accounts clearly before wiring automations so a future administrator can identify the intended account.
Mailbox synchronization and inbox triage can inspect every active credential for a mailbox connector. A second mailbox does not have to become the default before these operations can find it. When you compose a new email, the **Inbox** field lists each mailbox by its name, and the email leaves from the one you choose. Replies in that conversation, including a retry of a failed send, leave from the same mailbox.
## Rotate a secret or pause access
Use the row's replacement action for its method, such as **Replace API key** or **Replace token**. The new secret replaces the stored one while preserving the credential's name, default choice, and references. Test an appropriate service action after replacement.
**Disable** pauses a credential while retaining its configuration; **Enable** restores it. **Edit credential** handles other editable details, including its name or instance address where supported.
Deleting credentials removes access for automations and agents that depend on them. Move callers first. Deleting a connector's default makes its oldest remaining active credential the default, and the confirmation names that credential before you delete. A disabled credential or one that needs Reconnect never takes over, so with no active credential left the connector has no default until you choose one. Deletion cannot be undone by reopening the same row.
## Prepare an OAuth app
Owners and Admins use **OAuth apps** at the bottom of the page to configure the vendor app registrations used during consent. An organization app overrides the deployment-wide app. If neither exists, the connector cannot start authorization and the page shows that it is not configured.
Select **Configure**, enter the vendor's client ID and secret, and register the exact redirect URIs shown in the dialog with the vendor. Microsoft apps may also require a directory/tenant ID. On a later edit, leave a stored secret blank to keep it.
The Google Drive app also supports Knowledge import. The OneDrive/SharePoint import entry is for Knowledge rather than a separate connector. Slack's app is configured by the deployment operator. Read the relevant [connector guide](https://docs.tale.dev/platform/connectors/overview) before assigning vendor permissions.
For OneDrive/SharePoint, **Use Entra ID SSO app** can copy an existing SSO registration into the import configuration. This is a one-time copy: after rotating the SSO secret, copy it again and review the redirect URI and delegated permissions listed by the confirmation.
## Reconnect or diagnose a failure
**Reconnect needed** means stored OAuth authorization can no longer refresh. Choose **Reconnect** in that row's menu and authorize the same account again. Tale renews exactly that credential: its name, default choice and references stay, and no other credential changes. Reconnect does not re-enable a deliberately disabled credential; **Enable** returns it to service.
If the credential is removed before consent completes, or your role no longer allows you to manage credentials, Tale saves nothing and shows the reason. For Slack, authorize the workspace the credential already connects. Tale refuses a different workspace; connect that one with **Add credential**.
If the connection cannot start, check whether the OAuth app is configured. If the vendor rejects the return to Tale, compare the registered redirect URI with the exact URI shown by Tale. If an action fails after connecting, check the account's permissions and the required scope for that action.
For systems without a built-in connector, see [MCP and custom integrations](https://docs.tale.dev/platform/connectors/mcp-servers). Registering an arbitrary outbound MCP server is not part of this credential page.
# Enterprise SSO and provisioning
Source: https://docs.tale.dev/platform/admin/enterprise-sso
Enterprise SSO lets members sign in through your identity provider (IdP). SCIM lets that provider create, update, and deactivate members without waiting for them to sign in. An organization has one connection; you can enable sign-in, provisioning, or both in **Settings > Enterprise SSO** as an Admin or Owner.
## Before you start
You need permission to register an application with your IdP, its client credentials or SAML metadata, and the public Tale address members will use. Keep a working administrator session open while testing so you can correct the connection if a test sign-in fails.
Choose a **Display name** members will recognize. When more than one organization on the deployment has SSO enabled, it appears in the organization picker on the public sign-in page, so avoid internal or confidential information.

## Choose the protocol
| Protocol | Use it when | Information to prepare |
| --- | --- | --- |
| **Microsoft Entra ID** | Your organization uses Entra; optional team sync uses Microsoft Graph. | Tenant issuer URL, client ID, client secret. |
| **Generic OIDC** | Your provider supports OpenID Connect discovery. | Issuer URL, client ID, client secret. |
| **OAuth2** | Your provider has no OIDC discovery document. | Client credentials and authorization, token, and userinfo endpoint URLs. |
| **SAML 2.0** | Your IdP uses SAML assertions. | IdP metadata or its entity ID, sign-on URL, and signing certificate. |
## Connect an OIDC or OAuth2 provider
1. Select the protocol in Tale and open **Setup guide** to find the callback URL.
2. Register a web application with your IdP. Copy the callback URL exactly, including scheme, host, and path. Register each additional callback URL Tale shows if members use several deployment domains.
3. Enter the client ID and secret in Tale. For OIDC, enter the issuer URL; Tale discovers the endpoints. For OAuth2, enter the three endpoint URLs yourself.
4. Review **Scopes** and **Advanced**. Request the identity claims your provisioning rules need. Map nonstandard claim names where necessary; claim paths can use dots, such as `realm_access.roles`.
5. Select **Test connection**, resolve any error, then **Save** in the header. Continue with a real sign-in test below.
For Entra, use a tenant-specific issuer such as `https://login.microsoftonline.com/{tenant-id}/v2.0`, register the callback as a Web redirect URI, and copy the client secret's value rather than its ID. Microsoft's [application registration guide](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) explains the provider-side setup. Group-to-team sync requires the Microsoft Graph `GroupMember.Read.All` permission and admin consent. App roles you define in the app registration and assign to users or groups in the enterprise application arrive in the sign-in token, so an **App role** rule matches the role's **Value** (for example `Administrator`) and needs no Graph permission.
For Google, choose **Generic OIDC** with issuer `https://accounts.google.com`; see Google's [OpenID Connect setup](https://developers.google.com/identity/openid-connect/openid-connect). Standard Google OIDC does not provide group memberships, so signing in with Google alone does not enable group-to-team sync.
Microsoft 365 file import has a separate consent flow under Knowledge. Do not add `Files.Read` or `Sites.Read.All` to SSO scopes just to let members sign in. Configure import access through [connector OAuth apps](/platform/admin/connectors).
## Connect a SAML provider
1. Choose **SAML 2.0**. Copy the **SP metadata URL** and **ACS (reply) URL** into your IdP's SAML application. Use the service-provider metadata for its entity ID/audience and set the Name ID format to email address.
2. Under **Import IdP metadata**, import the IdP's metadata URL or select **Upload XML**. Review the entity ID, sign-on URL, and signing certificate filled in by the import. You can also enter them manually.
3. Under **Advanced**, map email, name, and groups if the IdP uses different attribute names. Keep **Require signed assertions** enabled.
4. Save the connection, then test a sign-in through the IdP.
If your IdP encrypts assertions, add a matching **SP certificate (PEM)** and **SP private key (PEM)** under **Advanced**. The certificate is published in SP metadata; the private key is stored as a secret and is not shown again. Configure the IdP to encrypt before enabling **Require encrypted assertions**. Tale refuses that setting without a decryption key and rejects unencrypted assertions when it is enabled.
Both IdP-initiated and Tale-initiated SAML are supported. For a sign-in started in Tale, finish in the same browser so the callback can validate the cookie created at the start.
## Assign roles and teams at sign-in
| Setting | What it controls |
| --- | --- |
| **Default role** | Role for newly provisioned members when no role rule matches; initially Member. |
| **Auto-assign roles from the IdP** | Maps groups, app roles, job titles, or claims to Tale roles. Review who could match an Admin rule before enabling it. |
| **Sync IdP groups to teams** | Creates or joins teams from group membership at sign-in. |
| **Exclude groups** | Comma-separated group names to leave out of team sync. |
Role rules are checked from top to bottom, and the first rule that matches decides the role. A person who matches several rules — for example, someone with both an `Administrator` and an `Employee` app role — gets the role of the rule higher in the list, not automatically the most privileged one. Put the most privileged rules first: drag a rule by its handle or use its **Move up** and **Move down** arrows, then save.
Team sync removes memberships it previously granted when groups disappear and deletes teams it created once empty. It preserves memberships created manually or through SCIM, and leaves excluded groups alone. See [teams](https://docs.tale.dev/platform/admin/teams) for manual membership management.
## Provision members through SCIM
1. In **SCIM provisioning**, select **Generate token** and copy it immediately; it is shown once.
2. Enter the token as a bearer credential and the displayed **SCIM base URL** in your IdP's provisioning configuration.
3. Provision a test user and group. Confirm the member and team appear in Tale, then test updates and deactivation before enabling a wider rollout.
SCIM Users map to members and Groups to teams. Deactivation (`active: false`) disables the member's access; reactivation restores their previous role. Deleting a SCIM user removes their organization membership but preserves their account. Provisioning them again starts with the connection's default role.
The organization owner cannot be deactivated or removed through SCIM. Groups can contain only members of this organization. A username change is refused if the new email is in use or the account belongs to multiple organizations, protecting its shared sign-in identity.
## Sign members in through an authenticating proxy
An application that already authenticates its users can hand them into this organization through its reverse proxy, so members never see Tale's sign-in form. The **Trusted headers** card on the same page holds the switch, the role ceiling and the keys the proxy presents.
1. Turn on **Accept sign-ins from a trusted proxy** and choose the **Highest role a proxy may assert**. The role header is capped at this role; Owner is never assertable. On every sign-in the member's seat follows the asserted role; an Owner seat never changes.
2. Select **Create key**, name it after the proxy that will hold it, and copy the key immediately; it is shown once. An organization holds at most 10 live keys.
3. Configure the proxy to send its sign-ins to the **Hand-off URL** with the key in the key header and the identity headers listed under **Header names**. A member who arrives through the proxy is signed in on the app's first request and never sees a sign-in page; the app offers them no sign-out, since the proxy owns that session. Pointing the proxy's `/log-in` at the same address remains supported.
To show the pages inside the application's own page, turn on **Allow embedding in a frame** under **Embedding** and list the page's origin under **Allowed origins**; Tale then admits that origin as a frame ancestor. A frame carries the signed-in session only when the surrounding page is on the same site as Tale.
The key decides the organization: a member signs in, an address Tale has never seen becomes a new member with the asserted role, and an existing account from another organization is refused. Turning the switch off refuses every key without revoking one. **Revoke** stamps a key so the proxy can no longer sign anyone in; sessions it already started stay signed in. The operator's [authentication configuration](https://docs.tale.dev/self-hosted/configuration/authentication) covers header names and proxy requirements.
## Verify and troubleshoot
Open a separate browser session and choose **Continue with SSO**. The organization picker appears only when more than one organization on the deployment has SSO enabled; select the organization by its display name there. Otherwise the button opens the one enabled organization's identity provider directly, and an address typed into the e-mail field is passed along only as a login hint — Tale does not route by e-mail domain. Complete sign-in, then check the expected role and team memberships. **Test connection** checks connection details; it does not prove that a real user receives the right access.
| Symptom | What to check |
| --- | --- |
| Redirect mismatch, including `AADSTS50011` | Compare the registered callback with Tale's exact URL; check domain, scheme, path, and trailing slash. |
| Connection test fails | Check issuer/endpoints, client ID, secret value and expiry, and required provider consent. |
| Browser-binding error | Start sign-in again in the same browser and allow the cookies needed across redirects. |
| Wrong role or missing team | Inspect the IdP's actual claims, role rules and their order, exclusions, and group permissions. An **App role** rule matches the app role's Value, not its display name or ID. |
| SCIM cannot connect | Check the base URL, bearer token, and whether provisioning is enabled. |
| Proxy sign-in is refused | Check that the card is on, the key is not revoked, and the proxy sends the email header and the key on the hand-off request. |
| Missing redirect URL or server-configuration warning | Ask the deployment operator to check [authentication configuration](https://docs.tale.dev/self-hosted/configuration/authentication). |
**Disable sign-in** stops new SSO sign-ins while keeping active sessions. **Remove** deletes the connection configuration and credentials. Arrange another working sign-in method before using either action.
# Audit logs
Source: https://docs.tale.dev/platform/admin/governance/audit-logs
Open **Settings > Governance > Logs** as an Admin or Owner to investigate recorded actions in your organization. Start with the event and time you need, then inspect its actor, target, result, and available change details.
## Find a change
1. Select **Audit logs** and open **Filter**.
2. Choose a category relevant to the action, such as member changes, security, or data.
3. Find the event by timestamp, action, and target. Open its row to inspect the details.
4. Check the status before interpreting the event: an attempted action marked denied or failed does not establish that the change succeeded.
The active tab and category are reflected in the URL, so you can bookmark the view. Access still depends on your organization permissions.

## Read an event
| Field | What to look for |
| --- | --- |
| Timestamp | When Tale recorded the action. |
| Action | The operation that was attempted or completed. Some newer actions appear by their technical name. |
| User | The person or system actor responsible for the action. |
| Resource and target | The kind of item and the particular record affected. |
| Category | The grouping used by the filter. |
| Status | Success, failure, or denied. |
| Detail view | Available previous/new state, changed fields, metadata, and error information. Not every event has every field. |
Treat the log as evidence of the events it records. It is not a complete copy of every conversation, provider response, or external service's activity.
## Choose the right tab
**Audit logs** contains individual events; the table loads more as you scroll, and its footer states how many events are loaded so far, so a count is never the whole history until the footer says so. **Sign-in blocks** helps investigate authentication lockouts. **Activity logs** summarizes activity and outcomes over a period: the period chosen in its **Filter** (7, 30, or 90 days) is named above the totals, and every number on the tab covers that period only. **Error logs** focuses on failures; its category filter helps narrow the investigation.
When a member cannot sign in, begin with the sign-in blocks and the [account security guidance](https://docs.tale.dev/platform/admin/two-factor-authentication). When a configuration changed unexpectedly, use the audit event and its detail view.
## Export results
Set the category filter, then open **Export** and choose CSV or JSON. CSV provides flat columns for spreadsheets, including UTC timestamps, actor identifiers, resource identifiers, status, and errors. JSON preserves the fuller event objects, including available change payloads and integrity hashes.
Exports honor the category filter and contain at most 10,000 rows, newest first. They are generated on the server and downloaded through a temporary link. A filtered or capped export is a selection of evidence; it is not necessarily the entire audit history or a complete hash chain.
## Retention and integrity
Use **Verify now** in **Chain integrity** to check the stored audit chain. The panel shows its status and the latest automated check. If a check reports a break, preserve the reported details and investigate with the deployment operator before relying on that segment of history.
A successful check covers the retained records it examined; it does not establish an independently signed origin for the history. The [operator integrity guide](https://docs.tale.dev/self-hosted/operate/security/audit-log-integrity) explains the checks and their limits.
Hash chaining helps detect changes to stored records; it does not prove that every possible action was logged. Audit retention is configurable under [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits). Check the active policy and deployment bounds instead of assuming a fixed retention period. Recoverable audit records can appear in [Trash](https://docs.tale.dev/platform/admin/governance/trash); permanent cleanup limits the history available here.
Scheduled retention cleanup records each of its runs here as system events in the **Data** category: when the run started, how many records each category deleted, and whether the run completed or failed.
# Competences
Source: https://docs.tale.dev/platform/admin/governance/competences
Use **Settings > Governance > Competences** as an Admin or Owner to keep the organization's competence register. A competence is one of two things:
- A **platform capability** lets a member do one narrow thing that otherwise needs a higher role. Grant it to the account behind an integration instead of making that account an Admin, which would also let it manage members, single sign-on and passwords.
- A **qualification** is a name your organization's review policy can require of the person who approves a review.
A grant applies in this organization only. Tale records every grant and revocation in the audit log, and removing a member from the organization revokes their grants.
## Platform capabilities
| Capability | What it allows |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Export notifications** (`tale:notifications.export`) | Read the notifications another member can see through the REST API, so another application can mirror them. |
| **Act for another member** (`tale:rest.act-as`) | Name the member an API call answers a question or decides a review for. The task timeline and the audit log then show that person instead of the API key. |
| **Publish skills to the organization** (`tale:skills.publish`) | Share a skill with the whole organization even when the [skill sharing policy](https://docs.tale.dev/platform/admin/governance/policies-and-limits#skill-sharing) reserves that for Editors or admins. |
| **Call models over the API** (`tale:models.api`) | Call the organization's models from one's own tools with a personal API key, through the [model endpoints](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints), once the organization turns them on. It also opens **Settings > API** with its **REST** and **Models** tabs, to create the key and read the setup. |
Owners and Admins have all four through their role, and Developers also have **Call models over the API** through theirs. Any other member needs the capability granted here, for example a Developer account whose API key an integration uses for an export. Without it, the REST API answers an export or an `actor` with `403 ROLE_FORBIDDEN`, and the model endpoints answer `403 MODEL_API_FORBIDDEN`. The [API reference](https://docs.tale.dev/develop/api-reference#name-the-member-the-gesture-is-for) describes the export and `actor` requests.
**Export notifications**, **Act for another member**, and **Call models over the API** are used with a personal API key, so each also lets its holder create one under **Settings > API > REST**. The key acts with the holder's own role; the competence adds only its own right.
**Publish skills to the organization** matters only while the skill sharing policy reserves organization-wide skills. Under **Editors and above**, Editors and Developers already have it through their role. Without it, a member can share skills with their own teams only, and the skill editor, uploads, and the REST API refuse an organization-wide skill with `403 SKILL_PUBLISH_FORBIDDEN`.
**Call models over the API** takes effect only once the organization turns on the model endpoints under [Models](https://docs.tale.dev/platform/admin/governance/content-models#model-endpoints). A member who holds it can open **Settings > API** with its **REST** and **Models** tabs to create a personal key and copy the setup; the **MCP** and **WebDAV** tabs stay with Owners, Admins, and Developers. Once the grant is revoked, Tale refuses the member's next model call.
## Grant a competence
1. Select **Grant competence**.
2. Choose the **Member**.
3. Choose the **Competence**: a platform capability, or **Qualification**, then type the **Qualification name** your review policy uses. Names that start with `tale:` are reserved for platform capabilities.
4. Choose when it **Expires**: **Never**, **In 30 days**, **In 90 days** or **In 1 year**.
5. Optionally note the **Evidence**: why the member holds it, such as a certificate, a ticket or the system it serves. It stays in the register.
6. Select **Grant**.
The grant applies to the member's next request, so an integration does not need a restart. A member holds each competence once at a time: to change its expiry or evidence, revoke the grant and grant it again. If the member already holds it, the dialog says so and grants nothing.
## Revoke a competence
Select **Revoke** on the row and confirm with **Revoke**. The member loses the competence right away. The grant stays in the register as history with the status **Revoked** and the revocation date; point at the date to see who revoked it.
## Read the register
The list opens on **Active** grants. Use **Filter > Status** to include **Expired** and **Revoked** grants, or select **Clear all** to see every grant.
| Status | Meaning |
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Active** | The member holds the competence. The line below shows its expiry date, or **No expiry**. |
| **Expired** | The expiry date has passed, shown below the status. Grant it again if the member still needs it. |
| **Revoked** | An Admin or Owner revoked it, or a new grant replaced it after it expired. The revocation date is shown below the status. |
Removing a member revokes every active grant they hold — capabilities and qualifications alike — so a re-added member starts without them. A revoked grant whose holder has left the organization names them **Former member**. The register lists every active and expired grant, and the 1,000 most recent revoked ones as history.
An integration can check its own key with `GET /api/v1/me`: `capabilities.actAs`, `capabilities.notificationExport`, `capabilities.skillPublish`, and `capabilities.modelApi` say whether the key may use each capability, through its role or a grant; `capabilities.modelApi` also needs the model endpoints turned on.
# Models
Source: https://docs.tale.dev/platform/admin/governance/content-models
Use **Settings > Governance > Models** as an Admin or Owner to choose the models members start with and the models they may use. Defaults guide a choice; access rules enforce a restriction. Configure [provider credentials](https://docs.tale.dev/platform/admin/providers) first so the intended models are available.
## Set a default model
1. Under **Default models**, select **Add rule**.
2. Choose **Default** for the baseline, **Role** for a role, or **Team** for a team. Select the target when needed.
3. Choose a provider and model, then **Confirm**. Save the page's pending changes in the header.
4. Start a chat as a member of the target group with its model on **Auto**, and check the resolved model.
The default is used when there is no explicit model choice. A team rule takes precedence over a role rule, followed by the baseline default; when someone belongs to several teams with a rule, the first matching team rule in the table wins (see [How rules combine](https://docs.tale.dev/platform/admin/governance/policies-and-limits#how-rules-combine)). A default does not prevent someone from selecting another permitted model.
## Restrict model access
Under **Model access**, choose the mode and add rules for the users, teams, roles, or default scope you want to cover.
| Mode | Effect for a matching rule |
| --- | --- |
| **Allowlist** | Only listed allowed models may be used; a blocked model remains denied. |
| **Blocklist** | Models are permitted unless listed as blocked. |
Access resolves user rules before team rules, then role rules, then the default. Multiple matching team rules combine their lists; an explicit block still wins for that model. If no rule matches, the policy does not restrict that user. Add a baseline rule when you intend to cover everyone.
For chat, access is checked when a model is used, including an explicitly selected or pinned model. A configured default must also pass the check. If it is denied, automatic selection can fall back to an allowed model. The editor warns about a default that conflicts with access rules; resolve that warning so the intended default is actually used.
Test both cases after changing access: an allowed model should work and a denied model should be refused for the affected member. Testing only as the admin does not prove a role-specific rule.
### Let API keys call the organization's models {#model-endpoints}
**Model endpoints for API keys** lets people use the models this policy allows from their own tools, such as opencode, Claude Code, or scripts built on the OpenAI or Anthropic SDKs, with a personal API key over OpenAI- and Anthropic-compatible endpoints. It is off by default. Turn on the **Model endpoints for API keys** switch; the change saves at once.
- **Who may call.** Owners, Admins, and Developers through their role. Any other member only with the competence **Call models over the API**, granted under [Competences](https://docs.tale.dev/platform/admin/governance/competences).
- **Which models.** The chat models your provider credentials serve with an API key or an environment variable, narrowed by each credential's model allowlist. The access rules above apply to every call, for the person whose key sent it. The switch works independently of **Enable model access policy**: with that policy off, only the credentials' allowlists narrow the list.
- **What each call passes.** The budgets under [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits) and the input guardrails under [Guardrails](https://docs.tale.dev/platform/admin/governance/guardrails#model-endpoints). The models' answers are not filtered.
- **Where it shows.** Each call is booked under the person and the key, as **Direct API** in [Usage analytics](https://docs.tale.dev/platform/admin/governance/usage-analytics).

Turning the switch off refuses the next call with `403 MODEL_API_DISABLED`. [Use Tale from your editor or a script](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints) shows members how to connect their tools.
## Choose the image-reading model
A text-only agent needs help reading an image, such as a screenshot or scanned page. **Vision model** selects the model that describes it for the agent. An agent whose own model reads images reads them itself; the vision model still serves the image tools that scripts and coding agents call inside their sandbox, such as batch transcription of scanned pages, so every managed agent gets one when a reachable model exists.
Leave **Model that reads images** on **Automatic** to follow the available provider catalog. Tale prefers a recommended vision model and otherwise selects a reachable low-cost option. The text below the picker identifies the current choice and reason.
Pin a model if you need a stable choice. The picker offers models that can read images. If a pin later becomes unavailable, restore its provider access or explicitly choose **Automatic** and save. Tale does not silently switch a pinned model. Review the current choice after rotating credentials or changing model availability.
## Let agents generate images
**Image generation** lets [project agents](https://docs.tale.dev/platform/projects/project-agents) working on tasks and agent nodes in [automations](https://docs.tale.dev/platform/automations/concepts) create images, such as a cover for a report or a visual for a campaign. It is off until you turn it on. Chat never creates images: a member who needs one assigns a task to a project agent.
1. Turn on **Let agents generate images**. The switch saves at once.
2. Leave **Image model** on **Automatic**, or select a model and save the page's pending changes in the header.
3. Check the line below the picker. It names the model agents use.

**Automatic** uses the first model of a short recommended list that your provider credentials reach: Gemini 2.5 Flash Image, GPT Image 1 Mini, GPT Image 1, then FLUX.2 Pro. It considers OpenRouter and OpenAI credentials. The picker lists every image model your credentials can serve, including those of other compatible providers. A selected model stays fixed until you change it; if it becomes unavailable, Tale reports it and does not switch to another model. Turning image generation off keeps the selected model for the next time you turn it on.
While image generation is on and a model is available, every agent turn that starts on a runtime with Tale's MCP channel gets an image tool. Agents on other runtimes, and every agent while image generation is off, do not see the tool at all; [Choose an agent runtime](https://docs.tale.dev/platform/agents/harnesses) shows which runtimes have the channel. Turning image generation off also stops a running agent's next image request. An agent saves its images among its files, so a task's images appear with its deliverables and an automation step's images with the step's output.
Images come in three shapes: square, landscape at 3:2, and portrait at 2:3. An agent that asks for another shape, such as a 16:9 banner, gets the one with the same orientation. The image model decides the exact size in pixels, and the image tool tells the agent the size of each image it saved.
Each image is billed to your organization and counts against the person who started the run, like the rest of the run. One agent turn creates at most 16 images, one request at a time, and its images draw on the same allowance as the turn's model use: each image's cost comes out of what the model may still spend, and once the turn has used the allowance up, Tale refuses the next image. A budget limit that applies to that person refuses the image before the image model is called. Set cost or request limits for images under [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits); [How usage is counted](https://docs.tale.dev/platform/admin/governance/usage-attribution) explains who each image counts against. For the policy file and custom image endpoints, see the [self-hosted provider reference](https://docs.tale.dev/self-hosted/configuration/providers#configure-image-generation).
## Provide a standard agent {#standard-agent}
Only Editors and higher roles can add agents to a project. So that a project without agents can still take on agent work, Tale offers the organization's **standard agent** there: anyone who can open the project can give it a task, Members included. It is on by default.

- **Where it appears.** In a project without agents, **Assignee** offers **Standard agent**, and [Create task from chat](https://docs.tale.dev/platform/chat/basics#create-task-from-chat) assigns it for you. Tale sets it up in a project the first time someone chooses it there or hands a chat's task to that project; the project's **Agents** tab then lists it with the **Standard** badge, and [The standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent) explains how it behaves there.
- **What it runs on.** With **Agent type** and **Model** on **Automatic**, Tale picks a recommended model the person starting the task may use under your [model access](https://docs.tale.dev/platform/admin/governance/content-models#restrict-model-access) rules, and runs it on Claude Code, or on the agent type a subscription model is bound to. The line at the end of the section names what it runs on for you. Choose an agent type or a model to use it for everyone, then save the page's pending changes in the header.
- **What it knows.** Its built-in instructions tell it to do what the task asks with the task's files and comments, deliver a requested document, presentation, spreadsheet or PDF as a task output, write in the task's language, and ask in a comment when the task is unclear. Text under **Instructions** replaces them.
- **What it may use.** The document skills `docx`, `pptx`, `xlsx` and `pdf` that are available to the project, and none of the connectors, platform tools or secrets an Editor can grant to other agents.
Every run starts with the settings in force at that moment; a running agent keeps the ones it started with. A chosen model stays in effect even when it becomes unavailable, and so does one that a person's model access denies: the standard agent then doesn't start for them and says why, and Tale doesn't switch to another model. Choose another model or **Automatic** and save. When no model a person may use can run it, it doesn't start for them either: add a credential under [AI providers](https://docs.tale.dev/platform/admin/providers), or check their model access.
Turn off **Provide a standard agent** to stop offering it; the change saves at once and keeps your choices. Projects without agents then offer no agent, and tasks already given to a standard agent can't start until you turn it back on. For the policy file, see the [self-hosted provider reference](https://docs.tale.dev/self-hosted/configuration/providers#configure-the-standard-agent).
## Choose the audio transcription model
**Audio transcription model** controls server transcription for audio and video attachments, the audio fallback for video links, and dictation in browsers without built-in speech recognition. Browser speech recognition uses its own service and keeps priority when supported.

An active default credential for OpenRouter also makes its dedicated speech-to-text models available here. Tale discovers them from the OpenRouter catalog. Check that the credential’s allowed models include your intended transcription model, then use **Automatic** or select that model explicitly.
1. Under **Model that transcribes audio**, leave **Automatic** selected to let Tale choose an available compatible model, or select a specific provider and model.
2. Save the page's pending changes in the header. Until you save, the selection is a draft; discard it to keep the saved setting.
3. Check the current model shown below the picker. Test a short recording before relying on the setup for a longer upload.
A model change applies to new transcription work; completed attachments keep their existing transcript. Uploading the same bytes again reuses completed work for the same transcription target, but transcribes them again when the target provider or model differs.
An explicit selection stays fixed. If that model becomes unavailable, Tale reports it and does not switch to another model. Choose another available model or **Automatic**, then save. If no compatible model is available, configure an active credential in [AI providers](https://docs.tale.dev/platform/admin/providers) and check the credential’s allowed models. A temporary failure to check the configuration calls for a retry, not a new model selection.
If unavailable server transcription prevents a member’s attempt to dictate or attach audio or video, a dismissible dialog explains the problem. Settings actions appear according to their access; otherwise, they are asked to contact an admin. A temporarily failed availability check can be retried. For deployment-managed selection and custom audio endpoints, see the [self-hosted provider reference](https://docs.tale.dev/self-hosted/configuration/providers#configure-audio-transcription).
## Diagnose an unexpected choice
Check the member's roles and teams, the explicit chat selection, the matching default, the access rule, and the provider credential's model list. A model appearing in a catalog does not establish that the organization has usable credentials for it. Spending and token caps still apply through [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits).
# Data subject requests
Source: https://docs.tale.dev/platform/admin/governance/data-subject-requests
Use **Settings > Governance > Data subject requests** as an Admin or Owner to process a person's erasure request. Tale tracks the request, its approval and waiting period, and the result of the deletion process. Confirm the person's identity and the appropriate scope through your organization's process before filing.

## File a request
1. Select **File request** and choose the **Subject** by name or email. Check that you selected the intended account.
2. Select the **Lawful ground** and write a **Reason narrative** that explains the request and references your internal case.
3. Type `ERASE` exactly, then select **File request**.
4. Open the receipt and check its status, deadline, and next required action.
Erasure permanently removes covered data; it is not a move to Trash. The receipt records the affected categories and counts, including chats, documents and uploads, preferences, feedback, notifications, usage, and audit-identifier scrubbing. The erasure also deletes the separate sandbox workspaces of the [agent runs the person started](https://docs.tale.dev/platform/projects/tasks#agent-runs-a-member-starts), even while work still runs in them, and the receipt counts them under **Sandbox workspaces**. A workspace counts only once the sandbox service has deleted its files, which can take a while for a large one. Until then, or while deleting them keeps failing, the receipt reads **Partial** with `sandboxWorkspaces` among its failed passes; choose **Retry** later, and the erasure completes once the files are gone. On Kubernetes, a workspace counts once its volume is handed to the cluster's storage provisioner; with a StorageClass whose `reclaimPolicy` is `Retain`, the volume's files stay until an operator removes the volume, although the receipt counts the workspace. A sandbox service or device that is not yet updated to this version cannot confirm the deletion, so the receipt stays **Partial** until it is updated.
Tasks that name the person as **Reviewer** lose that designation. A task review still waiting on them moves to the first eligible human fallback, the task creator or project creator, and that person is notified. On an archived task the review moves without a notification. Review decisions the person already made stay on record without their name.
## Check the policy before filing
| Setting | Effect |
| --- | --- |
| **Cooling-off window (hours)** | A waiting period of 0–72 hours before execution. Admins can cancel while waiting. Zero allows immediate execution once other requirements are met. |
| **Require dual approval** | A different admin must approve before the cooling-off window begins. The filer cannot approve their own request. |
| **Daily limit per admin** | Limits each admin to 1–50 filings per day. |
Only the Owner can change this policy. Stronger safeguards apply immediately; weaker safeguards are staged for 24 hours so any admin can cancel the change. Review the effective settings and any pending-change banner before relying on a new value.
## Follow the receipt
| State | What to do |
| --- | --- |
| Pending / awaiting approval | Check whether another admin must approve or the cooling-off window must finish. Cancel or reject if the request should not run. |
| Running | Wait for the category results; do not file a duplicate request. |
| Completed | Review the recorded counts and retain the receipt with your case. |
| Partial | Inspect the skipped categories and errors. Resolve the cause before retrying. |
| Blocked | Inspect the [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold). Covered data remains protected. The receipt names the hold that still applies; once the hold is released it says so, and the erasure continues only when you choose **Retry**. |
| Failed | Read the failure details. Use **Retry** when available; a watchdog timeout may require a new request. |
| Cancelled | No further execution is scheduled by this receipt. File a new request if the case must resume. |
An open receipt for a subject can prevent a duplicate filing. Work from that receipt rather than creating repeated requests. A retry blocked at initial filing must satisfy the current approval and waiting-period policy again. A receipt awaiting approval keeps the approval requirement captured when it was filed: turning dual approval off later does not release it.
## Manage the deadline
The list shows the tracked deadline and whether it is overdue. Use **Extend deadline** for a justified extension while it is still available: the application permits one extension before the original deadline expires and records the reason and admin.
The deadline is a tracking aid. Your organization remains responsible for assessing the request and communicating with the person. Completing a Tale receipt does not by itself confirm deletion from unrelated external systems or backups.
## Verify the outcome
Open the receipt's category counters, errors, and audit timeline. A completed action, a held category, and a failed pass have different outcomes; record those distinctions in your case. Review [audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs) for the associated administrative events.
# Feedback analytics
Source: https://docs.tale.dev/platform/admin/governance/feedback-analytics
Use **Settings > Metrics > Feedback** as an Admin or Owner to review the feedback members submit in chat. Ratings tell you what people judged useful; their comments help explain why.
## Find the feedback you need
Choose a period, then narrow by feedback type, assistant, or model. The available windows are 1, 7, 30, and 90 days, plus all time; the initial view uses 7 days. Selecting an assistant or model in a breakdown filters the view. Clear the filter chips to broaden it again. Tale attributes each rating from the rated reply itself: the model and provider that answered, and the assistant the conversation runs under. The assistant is the one the conversation runs under when the rating is given, not necessarily the one that wrote the reply: after a conversation switches assistants, a rating on an earlier reply counts for the new assistant. A reply in a plain chat, outside any assistant, is listed as **Unattributed**.
Use **Comments only** to focus on written explanations. If no feedback appears, check the period and filters before concluding that nobody has rated a reply. Feedback is voluntary; an unrated answer is neither a positive nor a negative vote.
## Keep the signals separate
| Signal | What it tells you |
| --- | --- |
| Thumbs up/down | Whether a member found a particular reply helpful. An optional comment provides context. |
| Arena verdict | Which answer a member preferred in a specific pair, or whether the pair tied or both were bad. A verdict cast on two copies of the same model is counted under **Same model**, apart from the verdicts and the matchup table. |
Members can change or withdraw a rating. The dashboard reflects the current state, not a permanent count of every click. [Arena mode](https://docs.tale.dev/platform/chat/arena-mode) explains how members compare two answers.
## Compare results fairly
Read the number of ratings alongside the helpful ratio. One positive vote is weaker evidence than repeated feedback across the tasks a model actually handles. Compare similar periods and tasks before attributing a change to a model or an assistant configuration.
Use the assistant and model tables to locate the change, the trend to find its timing, and **Recent feedback** to read comments. For arena results, compare the same model pairing; a win against one model does not establish superiority over every model.
Pair this review with [usage analytics](/platform/admin/governance/usage-analytics). A model can cost less per request while requiring more retries to produce a useful answer.
## Understand partial results
Large windows can reach the aggregation limit of 50,000 entries. If Tale shows a partial-results notice, narrow the period before drawing conclusions. Retention and deletion also affect which ratings and comments remain available; this view is not a permanent archive of member feedback.
# Guardrails
Source: https://docs.tale.dev/platform/admin/governance/guardrails
Use **Settings > Governance > Guardrails** as an Admin or Owner to control how chat text is checked before and after a model call. Enabled layers run in order: content safety, personal-data detection, then external moderation. Start with one clear rule and verify its effect before broadening the policy.

## Add a content rule
1. Under **Content safety**, choose whether to check **User input**, **Model output**, or both.
2. Select **Add category**, give it a recognizable **Label**, and choose its **Mode**.
3. Add the words or phrases to detect, one per line. You can import a text list; review it before applying it.
4. Save the category, enable the intended category and layer, then save the page's pending changes.
5. Test with synthetic text containing a match and with ordinary text that should pass. Check **Recent events** and the visible chat outcome.
| Mode | What happens on a match |
| --- | --- |
| **Flag** | Records the detection and allows the message. Useful while tuning a rule. |
| **Mask** | Replaces matched text with the configured placeholder. |
| **Block** | Refuses the message. |
When several categories match, block takes precedence over mask, then flag. Word matching ignores case. Check variants and false positives that matter for your languages; one successful test does not establish complete coverage.
## Protect personal data
**PII protection** detects configured patterns such as email addresses, phone numbers, and identifiers. Select the relevant built-in types and any custom patterns, then choose the intended behavior.
Masking removes matched values from the text sent onward. In a chat, the masked text is also what Tale stores and shows as the message; the original wording is not kept. Blocking refuses a match. Tokenization replaces detected values with indexed tokens for the model and restores them in its reply. Tokenization is therefore useful for processing with reduced exposure, but it is not a promise that the final reply will contain no personal data.
A built-in identifier that is digits only, such as a Swedish passport number or a Ukrainian tax ID, is recognized only next to a word that names it, for example `passnummer` or `ІПН`. Order numbers, compact dates, and build numbers pass through. Each detection in **Recent events** names the pattern that fired, such as `se-passport`.
Test the formats you actually use with synthetic values. Pattern detection can miss unusual formats and can flag ordinary text. Check input and output separately.
## Add external moderation
The moderation layer sends text to a configured classifier, such as OpenAI, Azure, Perspective, or a custom endpoint. Configure its credentials, categories, and actions, and choose the directions it should inspect.
Decide what should happen if the provider is unavailable: fail-open allows the message, while fail-closed refuses it. Review provider errors and circuit-open events when unexpected refusals or unfiltered messages occur. Enabling this layer introduces another service that processes the text; use the provider and endpoint approved for your organization.
## Set organization instructions
**Custom instructions** adds organization instructions ahead of the chat assistant's instructions and ahead of every agent's own: project agents working tasks and agent nodes in automations. Members cannot edit this organization policy. Use it for shared behavior and terminology; use access rules and filters for restrictions that must be enforced independently of a model following prose instructions.
## Guardrails on the model endpoints {#model-endpoints}
When your organization turns on [model endpoints for API keys](https://docs.tale.dev/platform/admin/governance/content-models#model-endpoints), the input side of these guardrails also judges every request sent to them. Content safety, PII protection, and the moderation provider read every text the request carries before Tale passes it to the model: system and developer instructions, the person's and the assistant's turns, tool calls, tool results and tool definitions, and documents given as text. The caller writes every one of them, so none is trusted more than another:
- A block refuses the request with `400 MODEL_API_GUARDRAIL_BLOCKED`, and nothing reaches the model.
- A mask replaces the match, and the model receives the masked text.
- Tokenization cannot be honoured, because Tale relays the model's answer unchanged and cannot restore the values into it. While PII protection is on in **Tokenize** mode, every request is refused with `403 MODEL_API_GUARDRAIL_UNSUPPORTED`; choose **Mask** or **Block** if members use the model endpoints.
- A moderation provider that fails under fail-closed refuses the request with `503 MODEL_API_GUARDRAIL_UNAVAILABLE`, and so does a guardrail setting Tale cannot read.
- While a guardrail is on, a request with more than 2 MB of text is refused with `413 MODEL_API_TEXT_TOO_LARGE`.
Output is not filtered there: rules that check **Model output** do not apply, and the model's answers, streamed or not, pass unchecked, as do images and documents that are not text. Custom instructions are not added either; the model receives the request as the caller's tool sent it. Detections appear under **Recent events** like chat detections, each recorded under `model-api:` followed by Tale's own ID for the request, with the `X-Request-Id` the caller received kept beside it. Because tools resend the whole conversation with every request, Tale reuses its verdict on a text it judged within the last few minutes and records a detection once rather than on every resend.
## Review and tune
**Recent events** shows the latest 50 detections, blocks, and provider errors. Filter by layer or outcome and inspect category, direction, and time. Raw matched text is not stored in these events, so a row explains the detection without reproducing the sensitive match.
If a rule is too broad, adjust its category or patterns and repeat the synthetic tests. If a detection is absent, check that the layer, category, and intended direction are enabled. How long events stay depends on the **Chat filter events** category of the [retention policy](https://docs.tale.dev/platform/admin/governance/policies-and-limits). The category is off by default, and events are kept until an admin enables it. While it is enabled, scheduled cleanup deletes events older than its period plus the deletion grace period, up to 50,000 per night; a larger backlog, such as months of events when the category is first enabled, is cleared over several nights. Recent events and the guardrail figures under **Settings > Metrics > Chat health** only show the events still kept: if the period plus the grace period is shorter than 30 days, the older part of a 30-day view stays empty.
A [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold) protects events from cleanup: an organization hold keeps every event, and a member hold keeps the events raised in that member's chats. An event whose chat was permanently deleted before the hold has no owner left to protect it and is not kept.
# Legal hold
Source: https://docs.tale.dev/platform/admin/governance/legal-hold
A legal hold preserves covered data while a matter is open. Admins and Owners manage holds under **Settings > Governance > Legal hold**. Place the hold while the data still exists: it cannot recover records that were already permanently deleted.

## Place a hold
1. Select **Place legal hold**.
2. Choose the target: a user as custodian, or the whole organization. For a user hold, select the intended member.
3. Enter a reason that lets another admin understand what must be preserved. Link the hold to a matter if you are tracking a case.
4. Confirm, then check the target and reason in **Active holds**.
A placed hold takes effect immediately. Covered data is protected from retention and erasure; attempts to delete held content are refused. Use your organization's preservation process to decide the correct scope.
## Organize holds by matter
Use **Create matter** to group related holds under a case name and number. The matter's linked-hold count helps you check that the intended custodians are covered.
Closing a matter requests the release of its linked holds. It does not release them immediately: each request still needs the separate review below.
## Release a hold
1. On the active hold, choose **Request release** and record why preservation is no longer needed.
2. A different admin reviews the request and chooses **Approve** or **Reject**. The requesting admin cannot approve their own release.
3. After approval, check the cooldown shown in **Release requests**. The hold remains effective while the request awaits that cooldown.
4. Check **Release history** for the completed outcome and **Active holds** to confirm which holds remain.
Placing a hold takes one admin; releasing it uses two-person review and a delay. Approval is therefore not the same as completed release.
## Understand blocked deletion
A hold can block a person's erasure request, deletion of their covered chats or documents, and deletion of a folder that contains held files. Any active organization or member hold also prevents deletion of the organization itself.
If a deletion fails, inspect the relevant hold instead of repeatedly trying the action. Releasing one hold does not remove another overlapping hold, and release allows the applicable retention or erasure process to continue. An erasure receipt the hold blocked does not resume on its own: open it under Data subject requests and choose **Retry**.
## Review related requests
Use [Data subject requests](https://docs.tale.dev/platform/admin/governance/data-subject-requests) to inspect an erasure receipt blocked by a hold, and [audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs) to investigate recorded hold actions. The [retention policy](https://docs.tale.dev/platform/admin/governance/policies-and-limits) determines normal cleanup after preservation no longer applies.
# Operations metrics
Source: https://docs.tale.dev/platform/admin/governance/operations-metrics
Beside [Usage](https://docs.tale.dev/platform/admin/governance/usage-analytics) and [Feedback](https://docs.tale.dev/platform/admin/governance/feedback-analytics), **Settings > Metrics** holds four dashboards for Owners and Admins. **Chat health** shows whether assistant replies fail or are blocked, **Harness turns** whether agents working in the sandbox finish their turns, **Automations** how live automation runs end, and **Projects** how one project's tasks move. Each dashboard recomputes its figures from the records Tale still keeps for the period you choose.

## Check chat health
Open **Chat health** when members report failed or refused replies. Choose 1, 7, or 30 days under **Filter**; the page opens on 7 days.
The cards count **Assistant turns**, the replies the assistant produced in the period. **Error rate** is the share of those turns that ended in an error, and **Blocked rate** the share Tale refused to answer, for example because a guardrail blocked the message. **Guardrail events** counts the detections in the period and how many of them blocked a message.
**Turns over time** splits each day's turns into **Successful**, **Errors**, and **Blocked**, so you can see when a problem started. **Breakdown** gives each agent's and each model's share of the turns; a reply in a plain chat, outside any agent, counts as **Unattributed**.
Under **Errors**, **By error type** groups the failures, for example **Rate limited**, **Model not found**, **Credit exhausted**, or **Usage limit reached**, and **Recent errors** lists the latest ones with their model and agent. The type points to the next check: a provider credential or its quota under [AI providers](https://docs.tale.dev/platform/admin/providers), a model's availability under [Models](https://docs.tale.dev/platform/admin/governance/content-models), or a budget rule under [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits).
**Guardrails** breaks the guardrail events down by kind and by filter and charts detections, blocks, and filter errors per day. Those events come from the filters on [Guardrails](https://docs.tale.dev/platform/admin/governance/guardrails) and cover only the events retention still keeps.
## Watch harness turns
A harness turn is one piece of work an agent harness, such as Claude Code or Codex, carries out in the sandbox: a [project agent](https://docs.tale.dev/platform/projects/project-agents) working on a task, or an agent step in an automation. **Harness turns** shows whether those turns finish. Choose 7, 30, or 90 days; the page opens on 30 days.
The cards report **Total turns**, the **Success rate**, the **Timeout rate**, the **p95 duration**, which 95% of the turns finished within, and the turns **Stopped by user**. **By harness** repeats the turns, success rate, and timeouts for each harness, so a rising timeout rate points to the harness it comes from. [Harnesses](https://docs.tale.dev/platform/agents/harnesses) explains how each one runs, and [Sandboxes](https://docs.tale.dev/platform/admin/sandboxes) where their capacity is set.
## Follow automation runs
**Automations** counts live runs of your organization's automations; test runs appear on each automation's **Runs** tab but not here. Choose 7, 30, or 90 days; the page opens on 30 days. Each card compares its figure with the period of the same length before it.
**Success rate** is the share of finished runs that succeeded: runs that failed or were stopped count against it, while queued, running, and waiting runs do not count yet. **Avg duration** averages the finished runs, and **Failed runs** counts the failures. **Runs over time** shows the daily volume, **Status breakdown** the outcome of every run in the period, and **Top automations** the ten automations with the most runs, with their success rate, average duration, failures, and last run. Open a failing automation's **Runs** tab to find the node that failed; [Read automation runs](https://docs.tale.dev/platform/automations/execution-logs) walks through it.
## Review a project's delivery
**Projects** shows one project at a time: choose it under **Select a project**. If the organization has a single project, it is selected for you. Choose 7, 30, or 90 days; the page opens on 30 days.
| Figure | What it measures |
| --- | --- |
| **Completed** | Tasks that reached Done in the period, split by current assignee into tasks assigned to an agent and all other tasks. |
| **Avg cycle time** | The average time from a task's first move to In progress until it reached Done. A task that skipped In progress has no cycle time. |
| **Intervention rate** | Changes requested in review plus escalations (questions that agent steps in automations asked people), per agent run started in the period. |
| **Spend** | The cost of the project's agent runs, with how many started and how many failed. |
The charts below show the open tasks by status at the end of each day, the tasks created and completed each day, the cycle-time trend, each day's completions split into **Agents** and **Humans**, and the daily spend. That split, like the one under **Completed**, goes by each task's assignee as it is now, not by who completed the task. A task assigned to an agent counts for agents, although a person moved it to Done; a task assigned to a person, an automation, or no one counts for humans. Past days change too: when a task is later assigned to an agent or loses its agent, for example because the agent was deleted, its completion moves to the other side. A task created directly in Done or Cancelled gets a completion timestamp at creation, but daily completion throughput counts status-change events only, so that creation is not counted as a completion event.
A rising intervention rate with a steady number of runs means people are sending more work back or agents are asking more questions. Escalations come from agent steps in automations working on the project, and each one counts on the day it is raised, whether or not anyone answers. For work sent back, read the tasks in review before changing an agent's instructions under [Project agents](https://docs.tale.dev/platform/projects/project-agents); for questions, read the runs of the automation that asked them, as [Read automation runs](https://docs.tale.dev/platform/automations/execution-logs) explains.
## Read the figures correctly
- Every dashboard works from the records Tale keeps. Retention settings and deletions shorten the history, so an empty period can mean the records are gone rather than that nothing happened.
- A busy period can exceed what one pass reads: Chat health, Harness turns, Automations, and Projects each count at most the 5,000 most recent records of a kind in the period. When Tale shows a notice about recent activity, narrow the period before drawing conclusions.
- Cost figures are recorded application usage, not a provider's invoice; [Usage analytics](https://docs.tale.dev/platform/admin/governance/usage-analytics) explains the difference.
# Policies and limits
Source: https://docs.tale.dev/platform/admin/governance/policies-and-limits
Use **Settings > Governance > Policies & Limits** as an Admin or Owner to control resource use and data handling. Choose the section that matches the problem: spending, uploads, retention, feature availability, the notice members see in chat, who may share skills with the whole organization, or who receives inbound conversations.

## Add a spending budget
1. Under **Budget rules**, select **Add rule**.
2. Choose the scope and its target. Use a role for a group such as Editors, a team for a shared workload, a user for an individual, an API key for one credential, or the organization for a shared ceiling. The API key list offers every active key held by a member of the organization, named with its owner, so you can cap one person's script or coding tool.
3. Select a daily, weekly, or monthly period. Enter at least one positive token, cost, or request limit. Cost is entered in USD; an empty field leaves that dimension uncapped by this rule.
4. Optionally set **Warning threshold (%)** between 0 and 100 to warn before the cap is reached.
5. Select **Confirm**, save the pending page changes, and check the saved rule's scope, target, period, and limits.
For example, a monthly role rule can give Editors a USD 50 personal spending limit, while an organization rule caps everyone's combined spend at USD 500. These are example amounts, not recommended defaults.
A rule stays in the table after its API key stops working. **Target** still names the key and its owner, with a status: **Expired** while the expired key is still stored, **Disabled**, **Revoked** when this organization recorded its revocation, or **Former member** when the owner has left the organization. **Unavailable** means the key is known but no longer stored, without a recorded cause; expiry cleanup can produce this state. **Unknown key** marks a key this organization has no record of. Remove such a rule, or edit it and choose an active key.
Budgets apply to new billable work, including chat, voice output, managed agent runs, the images agents generate, and calls to the model endpoints. Tale checks every chat request before it runs — a sent message, a regenerated or edited reply, both sides of a model comparison, a message waiting for an attachment, and a send through the REST API — and refuses it once a cap that applies is reached, naming the cap and when it resets. Replies still being written hold what they may spend, so requests sent at the same moment cannot pass a nearly reached cap together. An agent's image request is checked the same way before the image model is called: while its images are being made it holds an estimated cost, and the check also counts what the agent's own run may still spend on its model. Each image counts as one request. Image generation needs cost or request limits because its usage is not measured as text tokens. A token limit counts input and output tokens, and input includes the part of a prompt that a provider read from or wrote to its cache. That part is priced differently from other input, so an agent that reuses a long prompt can reach a token limit while its cost stays low. Investigate warnings in [Usage analytics](https://docs.tale.dev/platform/admin/governance/usage-analytics).
## Understand which caps apply
Personal limits resolve each dimension from the most specific rule that defines it: user, then team, role, and default. When someone belongs to several teams with a rule, the strictest of those caps applies to them personally. Organization limits apply in addition. A team budget also caps the combined usage of the team's current members, even when a member has a more specific personal rule: a new member's usage in the current period counts at once, and someone who leaves no longer counts. API-key limits independently cap requests authenticated with that key, and those requests' usage counts toward the key; they do not cap unrelated in-app work.
Managed agent runs count against the person who started them. A run you start from a task, a comment, the REST API, or the MCP endpoint uses your personal and team caps, and a run started with an API key also counts toward that key. Runs that a schedule, a webhook, or an event started have no person behind them: only the organization's limits apply to them, and [Usage analytics](https://docs.tale.dev/platform/admin/governance/usage-analytics) lists them under **Automations (triggers)**. [How usage is counted](https://docs.tale.dev/platform/admin/governance/usage-attribution) explains the rule for every kind of work.
A call to the [model endpoints](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints) is checked like a chat request. Before it runs, Tale works out its worst case, the estimated prompt plus the most output it may produce at the model's catalog price, and refuses it with `429 BUDGET_EXCEEDED` when that worst case does not fit what is left under a cap that applies to the key holder or to the key. While the call runs, its worst case is held against those caps. Afterwards, the cost the model gateway measured is booked under the person and the key, and each call counts as one request.
If a request is refused unexpectedly, check all applicable caps and their periods. Increasing one personal limit does not remove an organization, shared-team, or API-key ceiling.
Members can check their own standing under [Settings > Usage](https://docs.tale.dev/platform/member/preferences#usage-limits). It lists each personal, team, and organization cap that applies to them with its current usage and next reset, without showing the rules themselves.
### How rules combine {#how-rules-combine}
Every policy here and under [Content & models](https://docs.tale.dev/platform/admin/governance/content-models) reads its rules the same way. The most specific scope wins: a user rule before a team rule, a team rule before a role rule, and a role rule before the default. When a person belongs to several teams that carry a rule, the team rules combine by what they are:
- A limit, such as a budget cap or a context-window cap, combines to the strictest value. Joining a lenient team never raises anyone's cap.
- A permission list, such as model access, combines as the union of the allowed models; a block in any of the rules still wins for that model.
- A single choice, such as the default model, follows the order of the rules in the table: the first matching team rule wins.
## Control uploads
**Upload policy** sets allowed and blocked extensions, allowed MIME types, maximum file size in MB, and total volume per user in GB. Use the types your workflows need and test an allowed file and a rejected file after saving.
A filename extension, content type, and size are separate checks. If an upload fails, compare all three with the policy. Check existing per-user storage when individual files fit but further uploads are refused.
## Set retention and recovery time
Under **Retention policy**, select **Edit** and configure the categories your organization needs. The summary shows effective values, including disabled categories and temporary-file cleanup. Disabling a category's scheduled retention does not prevent an explicit deletion or erasure request.
Check the deployment's minimum and maximum bounds before changing a period. Changes that require review or a delay appear as proposals or pending changes; read their effective time instead of assuming they apply immediately.
The deletion grace period is the recovery window for supported soft-deleted records. A positive value leaves time to restore them in [Trash](https://docs.tale.dev/platform/admin/governance/trash); zero permits immediate permanent cleanup. Not every category has a restore path. A [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold) protects covered data from cleanup.
For self-hosted deployments, [Retention configuration](https://docs.tale.dev/self-hosted/configuration/retention) explains the operator controls and category-specific behavior. Do not infer an archive guarantee from a disabled policy or a displayed period alone.
## Review feature controls
Feature controls include scoped context-window limits and the organization-wide voice-output switch. A context limit controls how much context can reach an AI reply; it is different from a spending budget. A limit below 200,000 tokens also applies to Claude Code agent runs, as set for the person who started the run: the agent compacts its conversation into a summary before it outgrows the limit, and Claude Code treats any limit below 100,000 tokens as 100,000. Turning off voice output prevents members from enabling it through their own defaults or conversation choices.
The custom-instructions default switch stores the organization default for members’ personal instructions: while it is on, every member’s custom instructions apply to their chat replies unless they turned the feature off themselves under **Settings > Preferences**. Organization-wide mandatory instructions are a separate setting under [Guardrails](https://docs.tale.dev/platform/admin/governance/guardrails).
## Show a confidentiality notice in chat
**Confidentiality notice** adds a short line under the chat message field for every member of your organization, such as a reminder not to share sensitive data. It stays off until you turn it on.
1. Turn on the **Confidentiality notice** switch. Members see the notice in chat right away, in their language; open chats update without reloading.
2. Optionally enter your own text in the language tabs **English**, **Deutsch**, and **Français**, up to 280 characters per language. Save the pending page changes.

Members see the text for their language. A tab marked **untranslated** has no text of its own: members reading that language see your English text, or the default notice when English is empty too, and the empty field previews that text. A red dot marks a language whose text is too long, and saving stays unavailable until you shorten it. Turning the notice off keeps your texts for when you turn it on again.
The notice is a reminder only. It does not check, block, or change what members send. To act on sensitive content, configure [Guardrails](https://docs.tale.dev/platform/admin/governance/guardrails).
## Decide who shares skills with everyone {#skill-sharing}
By default, every member can share a skill with the whole organization. Use **Skill sharing** to reserve that for fewer people: choose who may **Share skills with the organization**, then save the pending page changes.
- **Every member** keeps the default.
- **Editors and above** admits Editors, Developers, Admins, and Owners: the roles that equip agents.
- **Owners and admins only** admits Owners and Admins.

Owners and Admins can always share with everyone. To let one more person do it without a higher role, grant them **Publish skills to the organization** under [Competences](https://docs.tale.dev/platform/admin/governance/competences).
Everyone else can still create skills and share them with their own teams. They cannot create a skill for the whole organization, widen one of theirs to **Organization**, or change an organization-wide skill in place. They can narrow a skill of theirs to their teams, with other changes in the same save, or delete it. The rule applies in the skill editor, to zip and folder uploads, to automation packages that carry skills, and to the REST API. Each refusal appears in the [audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs) as **Skill publishing refused**.
A stricter setting does not narrow skills that are already shared with the organization. To review them, open **Settings > Skills**, choose **Filter > Visibility > Organization**, and check the **Created by** column. Narrow or delete the ones that should not stay shared with everyone.
A managed configuration release installs its skills as the member who deploys it. Before you choose a stricter setting, make sure that member can still share with everyone, through their role or the competence; otherwise the next release that carries an organization-wide skill is refused.
## Conversation routing
Use **Conversation routing** to assign new conversations by where they arrive. Add a rule, set its fields, then save it:
- **Arrives on**: **Any mailbox**, one mailbox by its name, or an API app. An API app is listed once it has synced a conversation.
- **Sent to**: the address the conversation was sent to. It is required for **Any mailbox**, optional for one mailbox, and absent for an API app. Address matching ignores case.
- **Route to**: a team, a person, or both.
A rule for `support@example.com` also catches plus-addressed mail such as `support+billing@example.com`, and a rule for the tagged address wins for that address. When several rules match, the most specific one applies: a mailbox with its exact address, then a mailbox with the base address, then an address on any mailbox, then a mailbox alone.
A team assignment makes the conversation visible to that team's members; a person assignment makes it visible to that person. When both are set, either membership grants visibility. Unassigned conversations are for Admin and Owner triage.
Rules apply when a new conversation arrives. They do not reassign an existing conversation when a reply joins it. If a rule points to a deleted person, team, or mailbox, the conversation still arrives without that routing assignment. Test with a new message and verify the resulting assignee.
## Configure sign-in limits separately
Password requirements, sign-in attempt limits, session idle timeout, and [two-factor policy](https://docs.tale.dev/platform/admin/two-factor-authentication) live under **Settings > Governance > Security**. An organization idle timeout can tighten the deployment's limit. For trusted-header authentication, coordinate session expiry with the proxy or identity provider, which can authenticate the member again.
# Trash
Source: https://docs.tale.dev/platform/admin/governance/trash
Use **Settings > Governance > Trash** as an Admin or Owner to recover records that are still stored after soft deletion. Permanent deletion cannot be undone here, and not every deletion in Tale goes through Trash.
## Restore a record
1. Open Trash and use **Filter > Category** to narrow the list, or leave it unfiltered to see all supported types.
2. Check the record's name, owner, type, and deletion time. These help distinguish records with similar names. A chat is listed under its title; the answer discarded in an [Arena](https://docs.tale.dev/platform/chat/arena-mode) comparison keeps the title of its conversation.
3. Select **Restore** on the row and review the confirmation.
4. For a retention-expired record, type `restore` exactly. Confirm the action, then look for the record in its original location: for example, its chat list or Knowledge.
The restored row disappears from Trash. Tale records the restoration in the audit log. If the row is no longer available, refresh the list: cleanup may already have permanently removed it.
## Understand the status
| Status | Meaning |
| --- | --- |
| **Trashed** | The record was soft-deleted and is still available for restoration. |
| **Expired** | Retention policy expired the record. Restoring it overrides that policy, so confirmation requires the word `restore`. |
**Expired** does not mean the recovery window has already ended. Retention marks records expired when their grace window starts; permanent cleanup follows once that window elapses.
The category filter lists the record types that pass through Trash: chats, documents, temporary files, message feedback, contacts, and external conversations. A chat's earlier versions travel with it: they are trashed and restored together with the chat and never appear as rows of their own. Other data, such as automation runs, usage records, audit records, and chat-filter events, is deleted directly or as part of a parent record's cleanup and has no restore action here.
## Check the recovery window
The organization's retention policy sets the grace period. With a positive grace period, supported expired records remain recoverable until cleanup removes them. A grace of zero allows immediate permanent cleanup. Check the active policy under [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits) instead of relying on an assumed number of days.
An empty Trash means there are no recoverable records in the current view. It does not prove that nothing has ever been deleted. Clear category filters before concluding a record is absent.
Trash never shows a failed read as empty. If the list cannot be loaded, an alert takes the table's place and offers **Try again**, instead of the empty state. If records are listed but the next ones or a refresh cannot be loaded, a notice above the list says so and offers **Try again**, and the count under the list no longer claims to show all records.
## Account for legal holds
A [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold) prevents covered data from being deleted by retention or erasure. It preserves data that still exists; it cannot recover data already permanently deleted. Check the hold and retention history when investigating why an expected record did or did not enter Trash.
# Usage analytics
Source: https://docs.tale.dev/platform/admin/governance/usage-analytics
Open **Settings > Metrics > Usage** as an Admin or Owner to understand which workloads consume AI resources. Start with the reporting period, then use the breakdowns to investigate a change in cost or volume.

## Investigate a usage increase
1. Open **Filter** and choose a **Period** of 7, 30, or 90 days. The initial view covers 30 days.
2. Compare total requests, total tokens, total cost, and active users. More requests and larger replies have different causes.
3. Choose the chart's metric and granularity in the filter menu to see when the change happened.
4. Inspect **Top assistants**, **Top models**, and **Per-user usage**. Select an assistant or model breakdown to narrow the view; remove its filter chip to return to the wider view.
Assistant names can include supporting work such as chat-title generation. A request count therefore does not always equal the number of messages members sent. Voice synthesis has its own **Top voice models** table.
**Per-user usage** attributes every request to a person: the member who sent the chat message or started the agent run, including runs started through the REST API or the MCP endpoint. Runs that a schedule, a webhook, or an event started have no person behind them. Their usage appears as one row named **Automations (triggers)**, which does not count as an active user. In **Top assistants**, a project agent and an automation each appear under their name. [How usage is counted](https://docs.tale.dev/platform/admin/governance/usage-attribution) explains the rule for every kind of work.
## Read cost alongside tokens
The dashboard uses recorded usage and metering information. Input and output tokens are separate, and services such as voice output or image generation may have different billing units. A token total alone cannot explain every cost. Input tokens include the part of a prompt that a provider read from or wrote to its cache. That part is priced differently from other input, so a run that reuses a long prompt can show many input tokens for a small cost.
An image an agent generated appears under its image model in **Top models** and under the agent or automation in **Top assistants**, as one request per image and without tokens. Its cost is the charge OpenRouter reported, or for OpenAI the list price of the image tokens OpenAI reported. A request the provider billed without returning a usable image counts the same way.
Treat the displayed cost as recorded application usage, not an invoice from your provider. Provider pricing, subscriptions, credits, and unmetered calls can affect how it compares with the bill. A displayed zero does not prove that a provider charged nothing.
## Respond to a budget warning
Use the same period and affected workload when investigating a budget notice. Find the user, assistant, or model behind the increase, then decide whether to change the workflow, choose another model, or adjust a cap under [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits).
Compare [feedback analytics](https://docs.tale.dev/platform/admin/governance/feedback-analytics) before making a model change solely for cost: lower spend is useful only if the results still meet the task.
## Understand missing history
Charts reflect the usage records Tale still retains. The organization and deployment retention settings determine the available history; there is no universal 365-day guarantee. Check the selected period, filters, and usage-ledger retention if expected activity is missing.
If the metrics cannot be loaded, the page says so and offers **Try again** instead of showing zeros or empty tables. The period and filters you chose stay as they are. If a refresh fails, the figures already shown stay, with a note that they may be out of date. If **Try again** keeps failing, ask the deployment operator to check that Tale's services are running.
# How usage is counted
Source: https://docs.tale.dev/platform/admin/governance/usage-attribution
Every AI request Tale makes for your organization is recorded once, against one person, and measured against the [budget rules](https://docs.tale.dev/platform/admin/governance/policies-and-limits) that apply to that person. This page explains who that person is for each kind of work, which limits the work counts against, and where you find it in [Usage analytics](https://docs.tale.dev/platform/admin/governance/usage-analytics). Members see their own share under [Settings > Usage](https://docs.tale.dev/platform/member/preferences#usage-limits).
## What counts as usage
Tale records a request whenever a model or a metered service runs for your organization: a chat reply, including a regenerated or edited one and both sides of a model comparison; the short model call that names a new chat; a turn of a managed agent working on a task or inside an automation; an image such an agent generates; voice output; the transcription of an uploaded recording; a metered connector call; and a call to the model endpoints with an API key. Each record carries the tokens or units used and the cost estimated from the provider's list price at that moment; for a model endpoint call, it is the cost the model gateway measured.
## Who a request counts against
The rule is the same everywhere: a request counts against the person who asked for the work. The door the request came through, such as the app, the REST API, the MCP endpoint, or the model endpoints, changes nothing about who that is.
| Work | Counts against | Also counts toward | Appears in Usage analytics as |
| --- | --- | --- | --- |
| A chat reply, or the title of a new chat | The member who sent the message | The API key, when the message was sent through the REST API | The assistant used; a title under `thread-title` |
| An agent run on a task | The member who started the run from the task, or with a comment or a task description that mentions the agent; for a run another agent or an automation step started, the member that agent's or automation's run counts against | — | The agent's name under **Top assistants** |
| An automation run someone started | The member who started it from the run list, the builder, a chat, a task, the REST API, or the MCP endpoint | The API key, when the run was started with one | The automation's name under **Top assistants** |
| An automation run a trigger started | Nobody: a schedule, a webhook, or an event has no person behind it | — | The **Automations (triggers)** row under **Per-user usage** |
| A project agent's run a schedule began, or one another agent started for such a run | Nobody, as for the schedule's own run | — | The **Automations (triggers)** row under **Per-user usage**, and the agent's name under **Top assistants** |
| An image an agent generates | The person the agent's run counts against: its starter, or nobody for a run a trigger started | The API key, when the run was started with one | The agent's or automation's name under **Top assistants**, and the image model under **Top models** |
| Voice output or a transcription | The member who requested it | — | **Voice output** or **Transcription** under **Top assistants**; voice output also under **Top voice models** |
| A metered connector call | The member whose request made the call | — | The assistant that made it, or **Connector** |
| A call to the [model endpoints](https://docs.tale.dev/develop/use-tale-from-your-editor#model-endpoints) | The member whose API key sent it | The API key | **Direct API** under **Top assistants** |
A retry of an agent run continues the run its starter kicked off, so its usage stays with that person. When an integration uses an API key to act for another member, the run counts against the member acted for, and the key's own limit counts it too.
## Which limits apply
- **Personal, team, and role limits** bind the person a request counts against. A run a schedule, a webhook, or an event started has no such person and is not measured against any of them.
- **Organization limits** bind every request, including trigger-started runs.
- **API-key limits** bind the requests authenticated with that key: the chat messages it sent, its model endpoint calls, and the runs it started.
When a limit is reached, Tale refuses the next request before it runs and names the limit. A managed agent turn is refused at its start; a turn already running keeps the allowance it was given. An image the agent asks for during its turn is checked on its own before the image model is called, so a reached limit refuses the image while the turn continues. The image also draws on the allowance of the turn that asked for it. [How rules combine](https://docs.tale.dev/platform/admin/governance/policies-and-limits#how-rules-combine) covers the case of several rules applying to one person.
## Three situations worth knowing
**A teammate mentions your agent in a task comment or a task description.** Posting the comment or saving the description starts a run, and that run counts against the teammate who wrote it, not against you as the agent's creator.
**A scheduled automation spends every night.** Its runs appear on the **Automations (triggers)** row. They never raise anyone's personal usage or the active-user count, and only the organization's limits can stop them. Set an organization cost or request limit when you need a ceiling for them.
**An integration uses an API key on behalf of a member.** The member's personal and team limits see the run, and so does the key's limit. Two ceilings apply, and the stricter one refuses first.
## What members see
**Settings > Usage** lists every limit that applies to the signed-in member with its current usage: the chats they sent, the voice output they requested, the model endpoint calls they made, and the agent runs they started, whichever way they started them. Shared team and organization limits appear there too, because they can be reached before a personal one.
# Members and roles
Source: https://docs.tale.dev/platform/admin/members-and-roles
Add people under **Settings > Members**, then choose the role that matches their work. Roles determine permitted actions; project access, teams, and conversation assignments determine which resources a person can reach.

## Add a person
You need an Owner or Admin account to manage members.
1. Open **Settings > Members** and select **Add member**.
2. Enter the person's **Email** and, optionally, **Name**.
3. Choose a **Role**. Start with Member for everyday use; the comparison below explains when to grant more access.
4. For a new Tale account, enter a **Password** that meets the displayed policy. If the email already belongs to an account, Tale reuses its credentials and hides the password field.
5. Select **Add member**. For a new account, save the credentials shown in the confirmation before closing it, then share them through your organization's approved channel.
The person appears in the member list. Tale does not send an invitation or password-reset email for this flow: adding someone is your confirmation of their address, so the account works everywhere at once — including applications people sign in to with their Tale account. If the address is already a member of this organization, the form reports that instead of adding a duplicate.
After adding a person, assign the teams they need. A role alone does not put them into a team's project access or conversation queue.
## Choose a role
| Role | Typical work | Organization administration |
| --- | --- | --- |
| **Owner** | All product and administration work | Includes transferring ownership and deleting the organization, which also deletes its sandboxes and their files; deleting asks you to type the organization's name before the button enables. |
| **Admin** | Manage people, services, policies, and the team's work | Full organization settings; cannot transfer ownership. |
| **Developer** | Build agents, automations, and integrations | Technical settings such as providers, connectors, and API access; no member administration. |
| **Editor** | Maintain content and operate day-to-day work | Content editing; no access to **Automations**; connector resources are read-only. |
| **Member** | Use chat, read resources shared with them, and create project tasks, working their own tasks with project agents | No organization administration; can submit message feedback. |
| **Disabled** | No active access | Retains the membership record without granting permissions. |
Only Owners, Admins, and Developers see **Automations**, in the navigation and in projects; anyone else who follows a link to an automation sees **Access denied**.
People who are not Owners or Admins cannot open **Settings > Members**; they see their own role under [**Settings > Account > Your role**](https://docs.tale.dev/platform/member/preferences#role).
Project tasks follow a rule of their own: every member who can open a project creates tasks in it and changes the tasks they created or that are assigned to them, while Editors and higher roles change every task. The project's settings, agents, and files stay with Editors. [Manage tasks on a project board](https://docs.tale.dev/platform/projects/tasks#who-can-create-and-change-tasks) lists what a Member can do with their own tasks.
These are role capabilities, not a promise that every record is visible. Conversation reads follow assignment: a person sees work assigned to them or their teams; unassigned conversations remain with Owners and Admins for triage. See [conversation routing](https://docs.tale.dev/platform/admin/governance/policies-and-limits#conversation-routing).
Audit-log viewing is restricted to Owners and Admins. Actions by other roles can still produce audit entries; producing an entry does not grant access to the log.
## Change a role or reset a password
Open the person's row menu, choose **Edit**, and change **Role**. Select **Save**, then check the role badge in the list. To restore a disabled member's access, explicitly select the role they should receive.
The edit dialog also changes the display name. Email is read-only. To set a replacement password, enable **Update password**, enter a password that meets the displayed requirements, and save. Use your organization's identity-checking process before resetting an account.
You cannot edit your own role through this menu, assign Owner through the role picker, or demote the last administrator. Existing Owners and the organization creator also have protected roles. If a change is refused, check the relevant account before trying a different role.
## Transfer ownership
An Owner can choose **Transfer ownership** from another member's row menu. Read the confirmation carefully: the selected person becomes Owner and the transferring Owner becomes Admin. Use this action for an ownership handover, not an ordinary role promotion.
## Remove or recover access
Use **Disabled** when access should stop while the membership remains. Use the row's **Delete** action to remove the membership from this organization. Removing it also deletes the separate sandbox workspaces of the [agent runs the person started](https://docs.tale.dev/platform/projects/tasks#agent-runs-a-member-starts), with their files; **Disabled** keeps them. Review shared work and team responsibilities first; removing membership is different from a [data subject erasure request](https://docs.tale.dev/platform/admin/governance/data-subject-requests).
If a member loses an authenticator or passkey, open **Edit** and use the relevant security controls. [Two-factor authentication](https://docs.tale.dev/platform/admin/two-factor-authentication) explains reset, recovery, and session consequences.
# Admin
Source: https://docs.tale.dev/platform/admin/overview
Use organization settings to give people access, connect the services they need, and control how Tale handles work and data. Start with the task you need to complete; you do not have to configure every area before your team can begin.
## Set up a working organization
1. [Add members and choose their roles](https://docs.tale.dev/platform/admin/members-and-roles). Give each person the access their work requires.
2. [Create teams](https://docs.tale.dev/platform/admin/teams) when several people need the same project or conversation access.
3. [Connect an AI provider](https://docs.tale.dev/platform/admin/providers) so chats and agents have models available.
4. [Add connector credentials](https://docs.tale.dev/platform/admin/connectors) for the external services your workflows use.
Owners and Admins manage organization settings. Developers can reach the technical settings needed for integrations, but cannot manage members or open the full governance area. Personal account settings remain separate.
## Choose the right control
| You need to… | Open… |
| --- | --- |
| Choose default models or restrict model choices | [Models](https://docs.tale.dev/platform/admin/governance/content-models) |
| Limit usage, uploads, or retention | [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits) |
| Filter messages and set organization instructions | [Guardrails](https://docs.tale.dev/platform/admin/governance/guardrails) |
| Investigate actions or spending | [Audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs) or [usage metrics](https://docs.tale.dev/platform/admin/governance/usage-analytics) |
| Find failing chat replies, agent turns, or automation runs | [Operations metrics](https://docs.tale.dev/platform/admin/governance/operations-metrics) |
| Recover retained data or preserve it against deletion | [Trash](https://docs.tale.dev/platform/admin/governance/trash) or [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold) |
| Review an erasure request | [Data subject requests](https://docs.tale.dev/platform/admin/governance/data-subject-requests) |
## Sign-in, integrations, and appearance
Configure [enterprise SSO](https://docs.tale.dev/platform/admin/enterprise-sso) for your identity provider and [two-factor authentication](https://docs.tale.dev/platform/admin/two-factor-authentication) for account protection. Use [API keys](https://docs.tale.dev/platform/admin/api-keys) when software needs to call Tale.
[Branding](https://docs.tale.dev/platform/admin/branding) changes the organization's logo and colors. [Sandboxes](https://docs.tale.dev/platform/admin/sandboxes) shows execution capacity and workload limits; with [sandbox devices](https://docs.tale.dev/platform/admin/sandbox-devices), your organization's sandboxes run on machines of your own. For a project agent's access to these resources, read [Agents (admin view)](https://docs.tale.dev/platform/admin/agents).
After an upgrade, [What's new](https://docs.tale.dev/platform/admin/changelog) helps you check which platform changes affect your team.
# AI providers
Source: https://docs.tale.dev/platform/admin/providers
Connect an AI provider before asking Tale to run chats or agents. Under **Settings > AI providers**, Owners, Admins, and Developers manage the credentials their organization uses. The provider defines the connection and supported authentication; a credential supplies your organization's access to it.

## Add your first credential
1. Select **Add credential** and choose the provider. The catalog shows configured providers first; choosing one again adds another credential.
2. Choose an **Authentication method** if the provider offers more than one.
3. Check the **Name**. It starts as the provider's name, with a number added when another credential for that provider already uses it: `OpenRouter`, then `OpenRouter 2`. A name that identifies the purpose, such as `Production key` or `Finance team`, makes several credentials for one provider easier to tell apart. Fill in that method's required fields.
4. Review **Model allowlist**. For a provider with a catalog, leaving it empty allows the credential to use that catalog. Providers without a catalog need explicit model IDs.
5. Select **Add credential**. Check the new row and make it the default for that provider when ordinary requests should use it.
For **API key** or **Environment variable** credentials, open a chat and send a short test message with the intended model. A model must be available through an enabled credential and permitted by the organization's model-access rules. Verify subscription credentials with a task or automation agent, as described below. Saving a credential alone does not prove the provider will accept requests.
## Choose an authentication method
| Method | What you provide | When to use it |
| --- | --- | --- |
| **API key** | The provider's secret key | Standard metered API access. The stored secret is encrypted and later shown only as a masked fragment. |
| **Environment variable** | The name of a deployment variable | An operator manages the secret outside the UI. The name must start with `TALE_PROVIDER_KEY_`. |
| **Subscription key** | A supported vendor subscription secret | Runs through the vendor's supported agent runtime in tasks and automations, never in chat. |
| **Subscription broker** | A broker endpoint and token-response configuration | The deployment obtains usable subscription tokens from a broker for the vendor's agent runtime in tasks and automations, never in chat. |
Only methods supported by the selected provider appear. An environment reference does not create the variable: ask the operator to provision it using the [provider configuration guide](https://docs.tale.dev/self-hosted/configuration/providers).
## Use subscriptions in tasks, not in chat
A **Subscription key** or **Subscription broker** credential works only inside the agent runtime it is bound to: Claude Code for an Anthropic subscription and for the Moonshot and Z.ai coding plans, Codex for a ChatGPT subscription, Gemini CLI for Gemini, and Hermes for Nous Portal. Vendors do not permit subscription tokens in other applications, and Anthropic refuses them from any client other than Claude Code. Chat calls the provider's API from Tale itself, so it cannot use a subscription credential.
Tale therefore runs a subscription in tasks and automations, where its runtime works in a sandbox. Chat offers only models that an **API key** or **Environment variable** credential serves. The app says so in three places: the add form shows **Tasks and automations only** when you choose a subscription method, every subscription row in the credentials table carries the same note, and the chat model list names the subscriptions it leaves out.
To chat with a model you reach through a subscription, add an API key or environment-variable credential for the same provider, or connect a provider such as OpenRouter that serves the model.
## Connect a subscription broker
Subscription brokers support Anthropic subscriptions through Claude Code and OpenAI ChatGPT subscriptions through Codex. These credentials serve task and automation agents; chats require direct API credentials, for the [reasons above](https://docs.tale.dev/platform/admin/providers#use-subscriptions-in-tasks-not-in-chat).
| Provider and runtime | Target variable |
| --- | --- |
| Anthropic · Claude Code | `CLAUDE_CODE_OAUTH_TOKEN` |
| OpenAI · Codex | `TALE_SUBSCRIPTION_TOKEN` |
When adding the credential, choose **Subscription broker** and obtain the endpoint and authentication details from your operator. Use an endpoint that serves only the selected provider. For Tale AI gateway, that is `/api/tokens/anthropic` or `/api/tokens/openai`.
Set **Token array path** to `$.tokens`, **Token field** to `access_token`, and **Target environment variable** to the value above. Under **Advanced**, use `status` for **Status field**, `active` for **Active status value**, and `expires_at` for **Expiry field**, and leave **Expiry safety margin (ms)** at its default. The gateway itself holds back an account whose token is about to be refreshed while another account can take the work; Tale still uses it when no other available account can take the turn, for example while the others cool down after a rate limit. Other brokers may use different paths. OpenAI pools must also supply the vendor's `account_id` for every usable token; the broker's own `id` is a separate account identifier.
For OpenAI, restrict **Model allowlist** to model IDs your ChatGPT plan supports. The OpenAI API catalog can include models that the subscription cannot use.
To run a GPT-6 model on the ChatGPT subscription, save the OpenAI broker credential, then configure a [project agent](https://docs.tale.dev/platform/projects/project-agents) with **Agent type** set to **Codex**. Under **Model**, search for the model ID, such as `gpt-6.1-sol`, and select the entry marked **OpenAI · Subscription**. [Run the GPT-6 models](https://docs.tale.dev/platform/admin/providers#run-the-gpt-6-models) explains where else each model runs.
Choose **Token selection** according to how you want to distribute new agent turns:
- **Random**, the initial choice, picks uniformly from the usable accounts for each selection.
- **First usable** always takes the first usable account in the broker's order. Use it for an ordered preference, not to spread work.
- **Round-robin** picks the usable account that was selected least recently. All backend processes share the selection history for this organization and credential, including concurrent requests. Reordered responses and backend restarts preserve that history; stable broker account IDs also preserve it across token refreshes. This distributes selections; it does not promise equal token usage or equal numbers of running agents.
Save the credential, then run a short task or automation with the matching provider and agent runtime. Check that the agent completes a reply. If no account is usable, ask the operator to check account authorization, token expiry and planned refreshes, and reported quota. The [broker configuration reference](https://docs.tale.dev/self-hosted/configuration/providers#connect-a-subscription-broker) explains the optional account metadata, defaults and recovery.
## Run the GPT-6 models
The OpenAI catalog includes GPT-6 Astra (`gpt-6-astra`), GPT-6 Sol (`gpt-6-sol`), GPT-6 Luna (`gpt-6-luna`) and GPT-6.1 Sol (`gpt-6.1-sol`).
- **Chat** offers all four through an OpenAI **API key** or **Environment variable** credential, automatic model selection included. Astra and GPT-6.1 Sol make tool calls only through OpenAI's Responses API, so Tale calls them through it. GPT-6 Sol and Luna accept the chat's tools only with reasoning switched off, so the picker offers no effort levels for them.
- **Project agents and automations** run Astra and GPT-6.1 Sol only on **Codex**, the runtime that speaks the Responses API, on an API-key, environment-variable or ChatGPT-subscription credential. The model picker offers them for no other runtime.
## Configure Azure or another custom endpoint
Azure OpenAI requires **Endpoint URL**, typically `https://.openai.azure.com/openai/v1`. Each credential belongs to that resource. Azure model IDs are the deployment names configured in the Azure resource, so enter those names in **Model allowlist**; an empty list provides no models when there is no catalog to inherit.
Use the provider's documented endpoint and model identifiers. A display name from a marketing page is not necessarily the identifier accepted by its API.
## Define a custom provider
An organization can connect an endpoint the shipped catalog does not list: a self-hosted model server such as vLLM or Ollama, or an internal gateway that speaks the OpenAI or Anthropic API. Select **Add credential** and choose **Custom provider**, the entry pinned under the catalog:
1. Enter a **Provider name**. It names the provider and this credential, and the provider's identifier is derived from it.
2. Choose the **API format** the endpoint speaks and enter its **Base URL**, the API root the platform appends its paths to. A public host needs `https`.
3. Under **Models**, keep **Discover from the endpoint** to read the server's `/models` listing with this key, or choose **Enter model IDs** and list the exact IDs in the **Model allowlist** for an endpoint that cannot list them.
4. Fill in the **API key** (or the environment variable) and select **Add credential**.
The credential row now shows the provider with a **Custom** badge, and the provider appears in the **Add credential** catalog with the same badge; choosing it there adds another credential for it. A private or loopback address also needs the deployment's private-host opt-in, which an operator sets; saving the credential does not grant it. Ask your operator to prepare endpoint access and deployment policy, then follow [Connect a local model server](https://docs.tale.dev/tutorials/admin/connect-local-provider). If coding agents will use the provider, test an agent session too: their model traffic passes through a separate gateway, which also needs network access and certificate trust.
From the row's menu, **Check models** reads the endpoint's model list again with this key, **Edit credential** changes the base URL, API format and model source beside the credential's name, and **Delete** removes the credential. In the edit, **Save** changes the provider and the credential together: a duplicate name or a stale version is refused before either changes. If someone saved the provider or this credential after you opened the dialog, your save is refused instead of overwriting their change; reopen the dialog to load the current settings. Deleting a provider's last credential removes the provider itself, and the dialog says so beforehand. Every saved version of the definition stays in the organization's configuration history. Facts the form does not cover, such as a coding-agent endpoint, are set in the definition file the [provider configuration guide](https://docs.tale.dev/self-hosted/configuration/providers) describes.
If the connection drops during **Save**, the outcome may be unknown. Reload the page and check both the provider settings and the credential before retrying; a connection error does not prove that the save was rolled back.
## Select a default and restrict model access
Choose **Make default** from a credential's row menu. There is one default per provider; selecting another moves the badge. Disabled credentials cannot become the default. If no default exists, a caller must explicitly name a credential.
A credential's **Model allowlist** limits only that credential. [Models](https://docs.tale.dev/platform/admin/governance/content-models) sets default models and access rules for people, teams, and roles across providers. Both restrictions apply; widening one list cannot bypass the other.
The **Agent runtimes** section below the credentials is read-only. It shows available models and subscriptions for each runtime; change the credentials above to change that configuration.
## Recover a missing or failing model
- If a provider has no default, select the intended active credential and make it the default.
- If the catalog failed to load, use **Refresh catalogs** and inspect the provider's result. Live catalogs are cached; built-in catalogs change with the platform.
- If a model is missing, check both the credential's allowlist and organization model-access rules. For a catalog-free provider, check the exact model IDs.
- If a request is rejected, verify the credential's enable state, provider account access, endpoint, and provider-side quota before changing model rules.
## Rotate or retire credentials
Use the row's replacement action to rotate a secret while keeping its name and references. **Disable** pauses a credential without removing its configuration; **Enable** restores it. Confirm a replacement works with the intended model.
Deleting a credential removes access for callers that depend on it. Move those callers first. If you delete the default, select a replacement default so unnamed requests can resolve a credential. A credential the knowledge embedding model resolves — the one named under **Settings > Data residency > Embedding model**, or that provider's last active default — cannot be deleted: the delete dialog names the dependency and Tale refuses the delete. Choose another credential for the embedding model first.
# Run sandboxes on your own devices
Source: https://docs.tale.dev/platform/admin/sandbox-devices
A device is a machine you connect to your organization so its sandboxes run there instead of on the Tale server: a spare workstation, a build server or a Mac with capacity to share. Owners and Admins add and remove devices under **Settings > Sandboxes**. Developers can see the list.
## Check what the machine needs
- Linux on x86_64 or arm64, or macOS on Apple silicon or Intel.
- Docker: Docker Engine on Linux; Docker Desktop, OrbStack or Colima on macOS. If Docker is missing, the Tale CLI offers to install it.
- Outbound HTTPS to your Tale site. The device connects out to Tale; nothing has to reach the machine, so it works behind a router or firewall.
- Disk space for the sandbox images, several gigabytes, and for the workspaces it will hold.
By default a device runs one sandbox for every two CPUs and every 4 GiB of memory that Docker can use, up to 16 at a time, because each agent sandbox gets 2 CPUs and 4 GiB. You can choose another number when you connect the machine.
This number is a ceiling. The device also checks available host memory and workspace disk space before admitting work, so a free slot alone does not guarantee capacity for another sandbox.
A device runs your organization's work. Agent workspaces, the files they process and the short-lived credentials a task uses pass through the machine, and anyone with administrator access to it can read them. Connect only machines you control and trust as much as the Tale server. In turn, the Tale site decides what runs on the device, including the updates it installs by itself, so connect a machine only to a Tale site you trust with it.
## Add a device
Open **Settings > Sandboxes** and select **Add device** in the **Devices** section. Under **Install and connect**, select **Copy command**.
The command works once, within an hour. It carries a single-use token that the machine trades for a credential of its own.

Paste the command into a terminal on the machine and run it. It installs the Tale CLI and connects the machine:
```text
Connecting this machine to https://your-org.tale.dev as "studio-mac"…
Starting the sandbox device (Tale 0.5.60). The first start downloads the sandbox images, which can take a few minutes…
```
If the machine already has the Tale CLI, run the shorter command under **Already have the Tale CLI? Run this instead**.
As soon as the machine reaches Tale, the dialog shows **studio-mac is connected.** and the device appears in the **Devices** list as **Online**. Select **Done**.
The device keeps running after the machine restarts, as long as Docker starts with it. When Tale is updated, the device updates itself to the same release.
## Understand where sandboxes run
New agent and automation workspaces start on a connected device that has room. When none has, they start on the Tale server. A workspace stays with its files on the machine where it started, so an agent that already has a workspace on the server keeps using it. Pages rendered for website crawling always stay on the server.
When several devices have room, Tale considers both free slots and observed memory headroom. If a device refuses a create because it is full, another device or the server can take it. If the response is lost or the device returns a server error, retries stay on that device: it may already have created the workspace.
Once your organization has a device, the **Workspaces** list shows where each workspace runs: **On the server** or on a device by name. While a device is offline, work that needs one of its workspaces fails with a message that the device is not connected. Start the work again once the device is back online; workspaces never move to another machine by themselves.
Sandboxes on a device reach the internet through the machine's own connection, through the same egress proxy as on the server. The proxy blocks private network addresses, so sandboxes cannot reach other machines on your local network.
Connected devices also raise the ceiling for your organization's [workload limits](https://docs.tale.dev/platform/admin/sandboxes#change-a-workload-limit): their total may use the deployment capacity plus the sandboxes your devices run.
## Read the device list
| Column | What it shows |
| --- | --- |
| **Device** | The name the machine connected with, and its operating system. |
| **Status** | **Online**, **Updating**, **Update needed**, **Update failed** or **Offline**. |
| **Sandboxes** | Sandboxes running now, and how many the device runs at once. |
| **Machine** | CPUs and memory that Docker can use on the machine. |
| **Version** | The Tale release the device runs. |
| **Last seen** | **Now** while connected, otherwise when the device was last in contact. |
A device on another release than the server takes no new sandboxes until it updates. **Update needed** means the device does not update itself; **Update failed** means its last automatic update did not succeed. In both cases, run `tale sandbox update` on the machine.
## Look after a device from the machine
Run these commands on the device itself:
| Command | What it does |
| --- | --- |
| `tale sandbox status` | Shows the connection, the organization, the release and the sandboxes running now. |
| `tale sandbox logs --follow` | Follows the device's log. |
| `tale sandbox update` | Moves the device to the server's release now, with the server's current addresses for its sandboxes. |
| `tale sandbox disconnect` | Removes the device from its organization, stops its sandboxes and deletes their workspaces from the machine. Add `--keep-data` to keep the workspaces. |
## Remove a device
In the **Devices** list, open the device's row menu, select **Remove** and confirm. The device stops running sandboxes for your organization right away. Its workspaces stay on the machine but can no longer be reached from Tale, so their agents start with fresh workspaces the next time they run.
To clean up the machine as well, run `tale sandbox disconnect` on it.
## Fix a device that does not connect
- **The command has expired or was already used.** Select **Add device** again to get a new command.
- **The device started but has not reached the server yet.** Run `tale sandbox logs --follow` on the machine and check that it can open HTTPS connections to your Tale site, including through any proxy between them.
- **The connection fails with a certificate error.** The machine must trust your Tale site's TLS certificate. A deployment that uses a self-signed certificate cannot take devices.
- **Add device is unavailable, and the section says the sandbox service doesn't accept devices.** The deployment runs without its device hub. On a self-hosted deployment, the operator turns it on as described under [Sandbox devices](https://docs.tale.dev/self-hosted/configuration/environment-reference#sandbox-devices).
# Manage sandbox capacity
Source: https://docs.tale.dev/platform/admin/sandboxes
Open **Settings > Sandboxes** when agent work or crawling cannot obtain an execution environment. The page separates your organization’s workload limits from the deployment’s actual infrastructure. Owners and Admins can change limits and decide when unused workspaces are deleted; Developers can read limits and aggregate capacity, without the private workspace list.
## Identify the limit that matters
| Workload | Default | What uses a slot |
| --- | --- | --- |
| **Project agent sessions** | 2 | An agent workspace starting or doing work. The agent reuses its workspace across tasks. |
| **Workflow sessions** | 2 | A workflow run’s sandbox, shared by its sandbox work. Concurrent runs use separate slots. |
| **Render sessions** | 2 | A temporary sandbox rendering pages during website crawling. |
These are concurrency limits, not a count of tasks or a spending budget. An agent can work on several tasks in its one workspace. Limits do not reserve infrastructure for the organization; all organizations share the deployment capacity.

## Change a workload limit
1. Check the workload’s **Allocated** count and the infrastructure measurements below it.
2. Enter a whole number from 1 to 500 for the relevant limit. **Total organization sessions** recalculates the sum of all three fields.
3. Keep that total within deployment capacity, then select **Save** in the header. **Discard** restores the saved values.
4. Reopen the page to confirm the saved limits and inspect whether new work can obtain an allocation.
For example, the defaults total 6. If deployment capacity is 8, a total of 8 is valid and 9 is refused. The server rechecks capacity when saving, so another observation may differ from the one you first saw. Your organization's connected [devices](https://docs.tale.dev/platform/admin/sandbox-devices) add the sandboxes they run to the ceiling: with one device that runs 4, the total may reach 12.
Lowering a limit affects future admissions; it does not interrupt active work. If infrastructure data is unavailable, reductions remain possible but increases need a fresh capacity observation. If the operator lowered capacity below your existing total, reduce your limits before saving again. If organization allocation data itself cannot load, the fields remain unavailable instead of showing editable defaults.
## Read the infrastructure measurements

| Measurement | What it means |
| --- | --- |
| **Deployment sandboxes** | Running and starting environments across all organizations, compared with shared capacity. Kubernetes reports the namespace boundary. |
| **Your organization's sandboxes** | This organization’s running and starting environments, including idle ones kept for reuse. This is a count, not another organization limit. |
| **Host CPU usage** | Recently used and total CPU cores for the host, including its other services. |
| **Host memory usage** | Used and total host memory, including other services and allowing for reclaimable cache. |
Measurements refresh every 15 seconds; **Refresh** requests a new observation. Check its timestamp before interpreting it. CPU usage is the change between two samples; a first observation after a long gap takes about a second longer. Remote hosts may expose totals without usage. Kubernetes namespace access does not expose host measurements. **Unavailable** means unknown, not zero.
## Explain an allocated or idle workspace
Owners and Admins can inspect **Workspaces**. A row identifies its agent or workflow run, runtime state, allocation state and running tasks. These states answer different questions: a container may remain running for reuse after it has released its organization slot. A project agent’s workspace stays listed while the agent is idle, as **Stopped** with **Quota released**, until you destroy it or Tale deletes it: when nobody has used it for [the number of days your organization sets](https://docs.tale.dev/platform/admin/sandboxes#delete-unused-workspaces-automatically), or when [its agent, project or member is removed](https://docs.tale.dev/platform/admin/sandboxes#explain-why-a-workspace-disappeared). Once the agent itself is deleted, the row reads **Deleted agent** until Tale has deleted the workspace. A workflow run’s workspace is reclaimed shortly after the run ends.
**Spend** adds the metered cost of finished turns. A turn still running is included when it ends. Temporary crawler environments appear in capacity counts even without a standing workspace row.
When deployment capacity is full, Tale may reclaim an unpinned idle environment whose allocation is released and which confirms it has no ongoing work. Its persistent workspace files remain for the next start. Busy, pinned or unresponsive environments are not candidates. If there is no safe candidate, new work must wait for capacity. An environment whose task or run has finished stops after a few idle minutes, so its room frees soon after the work ends. An agent environment that runs Docker inside keeps its full idle time, so the next turn does not have to pull its images again.
## Manage an existing workspace
Owners and Admins can use a workspace’s row menu:
| Action | Effect |
| --- | --- |
| **Stop task** | Cancels all currently running operations in that workspace. Check the listed tasks first; one agent may have several. |
| **Pin** / **Unpin** | Keeps the workspace exempt from automatic idle and expiry cleanup and from deletion for being unused, or restores normal cleanup. A pinned allocation can continue holding capacity. If a pinned workspace’s environment disappears, for example after a host restart, Tale starts it again with its workspace files, and the workspace stays pinned. |
| **Destroy** | Asks for confirmation, then removes the sandbox and its workspace files in the background: it unpins the workspace first and cancels running work. Until the workspace is gone, its row reads **Destroying** and you can go on using the page. If Tale still cannot remove it after several attempts, the workspace remains listed, unpinned, and reads **Destroy failed**; choose **Destroy** again to finish removing it. While the row reads **Destroying**, nothing new starts in that workspace: an agent's run waits and reads **Waiting for a sandbox slot**, and a workflow run's step that needs the workspace fails at once with the reason, without waiting or trying again, because the files its earlier steps left are being deleted. Once the row is gone, the next start creates a fresh environment; if it reads **Destroy failed** instead, the waiting work continues in the old files. |
A pin change is saved durably and retried if its runtime acknowledgement fails, including after a worker restart or device outage. Retries apply the latest desired value to the same session incarnation; repeated unpin delivery does not extend its expiry.
Use stop when the current work should end but its files should remain. Before destruction, preserve outputs you still need and read the confirmation. Idle capacity reclamation preserves workspace files; explicit destruction and automatic deletion do not.
## Delete unused workspaces automatically
A project agent keeps its files between runs in workspaces: the one it reuses across tasks, and a separate one for each Member who [starts its runs](https://docs.tale.dev/platform/projects/tasks#agent-runs-a-member-starts). Tale deletes a workspace that nobody has used for the number of days your organization sets; **Destroy** still deletes one at once. Owners and Admins set this rule under **Workspace cleanup**, above **Workspaces**. Developers don't see that section.

1. Keep **Delete unused workspaces** switched on, as it is by default. While it is off, Tale deletes no workspace for being unused.
2. Enter **Days without use**, a whole number from 1 to 3650; the default is 30. The days count from the last time the agent worked in the workspace, or from when it was unpinned.
3. Select **Save** in the header. **Discard** restores the saved values.
A change never makes a workspace go early. After you switch deletion on or shorten the period, no workspace is deleted for being unused until the full number of days has passed since the change. The same wait follows the update that introduced the setting. Lengthening the period doesn't restart the wait; switching deletion off and on again does.
In **Workspaces**, a stopped agent workspace shows the day it will be deleted under its status: **Deleted on … unless used again**. A new run in the workspace starts the count again. A pinned workspace, or one a [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold) keeps, shows no date and is never deleted for being unused.
## Explain why a workspace disappeared
Besides deleting unused workspaces, Tale deletes a workspace when what it belongs to is removed, whatever the cleanup setting says:
- Deleting a project agent deletes all its workspaces, including each Member's. Deleting a project does the same for every agent in it.
- [Removing a member](https://docs.tale.dev/platform/admin/members-and-roles#remove-or-recover-access) from the organization deletes their own workspaces with every agent. Setting the member to **Disabled** instead keeps them.
- [Erasing a person's data](https://docs.tale.dev/platform/admin/governance/data-subject-requests) deletes their own workspaces without waiting for work running in them.
- Deleting the organization deletes all its sandboxes and their files, revokes the gateway keys issued to them, disconnects its [devices](https://docs.tale.dev/platform/admin/sandbox-devices) and removes the build and package caches kept for it.
This happens within about a minute, or after the task ends if one is still running in the workspace. A pinned workspace goes too, but a [legal hold](https://docs.tale.dev/platform/admin/governance/legal-hold) keeps every workspace it covers: a hold on the organization keeps all of them, and a hold on a person keeps that person's own workspaces. An hourly cleanup also deletes leftovers that nothing owns any more, such as a workflow run's workspace that was never reclaimed.
Every workspace Tale deletes on its own, for one of these reasons or for being unused, appears in the [audit log](https://docs.tale.dev/platform/admin/governance/audit-logs) under **Settings > Governance > Logs** as **Sandbox workspace deleted**. It is a system event in the **Data** category, and its metadata names the reason: `agent_deleted`, `member_removed`, `member_erased`, `unused` or `orphaned`. The event is recorded once Tale has confirmed that the workspace's files are deleted, which can take a while for a large workspace. On Kubernetes, it is recorded once the workspace's volume is handed to the cluster's storage provisioner, which deletes it according to its StorageClass's `reclaimPolicy`; the metadata says which happened (`deletion`: `done` or `handed_off`). If deleting the files fails, the event appears with the status **Failure**, and the deletion is tried again until the files are gone. A sandbox service or device that is not yet updated to this version cannot confirm a deletion, so Tale records no event for its workspaces and keeps trying until it is updated. A leftover that nothing owns any more can end without an event: if its files are still being deleted when the hourly cleanup moves on, the sandbox service finishes deleting them, but Tale does not check on them again.
## Resolve a blocked start
A start that finds no room waits. When your organization's limits are in use, or the deployment is full or short of memory, the work starts on its own once room frees, and needs nothing from you. Disk space is different: stopping idle work frees none, because a stopped workspace keeps its files. Tale removes build caches that no session uses, if there are any; when that frees too little, the start waits until workspaces are deleted, for example ones you destroy because you no longer need them, or until the operator frees disk space. When the deployment itself is full, its room goes to the work that has waited longest, whichever organization it belongs to, and each waiting start is told when its turn comes:
- A task run waits in the queue. It is tried again as soon as a session ends, in your organization or in another one on the same deployment. When the deployment is full, it is also tried when its turn comes. Every two minutes, Tale also tries a few of each organization's waiting runs, the longest-waiting first.
- An automation's agent step tries again without using up its retries: when its turn comes if the deployment is full, otherwise after a pause that grows while the step keeps waiting, up to about two minutes. The run shows **Waiting for a sandbox slot** meanwhile. After two hours without room, the run fails with that reason. A workspace an administrator is destroying is no lack of room: the step fails at once instead, as described for **Destroy** above.
- Crawling waits for its turn, then continues with its next batch.
Raise a workload limit only when its allocations are full and the new total fits shared capacity. If the deployment itself is full, increasing an organization limit cannot create infrastructure. To add capacity of your own, [connect a device](https://docs.tale.dev/platform/admin/sandbox-devices): new workspaces start on it. Otherwise, ask the operator to inspect capacity and host resources; a free container slot alone does not guarantee enough CPU, memory or disk space.
For a credential or model refusal, use [AI providers](https://docs.tale.dev/platform/admin/providers). For a spending refusal, use [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits). Self-hosted operators can inspect the deployment setting in the [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference#sandbox-infrastructure).
# Teams
Source: https://docs.tale.dev/platform/admin/teams
A team is a label on work, not a place you switch into. A document, folder, or project carries the teams that may see it, and a conversation can wait in a team's queue. A person's role controls what they may do; their teams decide which restricted work they can reach. Owners and Admins manage teams under **Settings > Teams**.

## Create a team
1. Select **Create team** and enter a **Team name**, such as `Customer support`.
2. Select the organization's members who should join. If you select nobody, Tale adds you to the team.
3. Select **Create team**. Check the new row and its member count in the list.
Choose a name that people will recognize wherever teams are shown: the audience of a document or project, a queue in the inbox, a list filter. The form accepts up to 80 characters. A name is unique within the organization: one that another team already uses — in any letter case or spacing — is refused. Creating the team does not put existing work under it; choose the team on the documents, projects, and conversations it should cover.
## Change membership or the name
Open a team's row to inspect its members. The row menu offers **View**, **Edit**, and **Delete**. Use **Edit** to change the name or membership, then save your changes and check the member count.
A team keeps at least one member. To remove the last one, delete the team instead.
A person can belong to more than one team. Their access can come from several teams or from a direct assignment, so removing them from one team does not necessarily remove all access to a resource. Review those other routes when withdrawing access.
Notifications follow the same rule. Someone who can no longer open a project gets no new notifications or emails about its tasks, not even about the tasks they watch. What they were already told stays in their notifications. If they regain access, the tasks they watch notify them again.
A team your identity provider provisions shows **Synced** in the list. The provider owns its name and members: the edit dialog shows them read-only, because a local change would be undone by the next synchronization. You can still delete such a team locally; the provider may recreate it.
Rename an existing team when its purpose changes but the same people should retain access. Deleting and recreating it creates a different team and changes existing resource assignments.
## What a team decides
Every team-scoped resource follows one rule. A resource with no team is visible to everyone in the organization. A resource with teams is visible to the members of any of those teams. Owners and Admins see everything either way, so a team is never a way to hide work from administrators.
| Resource | How teams matter |
| --- | --- |
| Projects | **Audience** on **General** lists the teams that can open the project; empty means the whole organization. |
| Documents and folders | A document or folder carries the teams that can read it. Anything filed into a team folder takes the folder's teams and cannot name a team outside them. |
| Skills | Team visibility makes a skill available to the selected teams. |
| Conversations | Assignment to a team places the conversation in that team's queue. |
When you restrict work to teams, you can only choose teams you belong to; Owners and Admins can choose any team of the organization. Team membership does not grant actions that a role forbids: an Editor and a Member in the same team can have different editing rights.
Members see their own teams under **Settings > Account > Your teams** and in the **Teams** row of the profile menu. To narrow a list to certain work, each list offers a **Teams** filter with **Organization-wide**, **My teams**, and every team by name; the inbox uses an **Assignee** filter instead, which covers people as well as teams. See [Manage your account](https://docs.tale.dev/platform/member/preferences#teams).
For inbound conversations, [routing rules](https://docs.tale.dev/platform/admin/governance/policies-and-limits#conversation-routing) can select the team when the conversation arrives. Without a person or team assignment, the conversation stays in administrator triage.
## Retire a team carefully
Choose **Delete** from the row menu. The confirmation counts the team's members, the projects, folders, and documents it is on, and the conversations in its queue. It also says how many of those items have no other team and will become visible to everyone in the organization. Reassign work that must remain restricted before you confirm.
Deleting a team cannot be undone. Every project, folder, and document drops the team and keeps its other teams. An item whose only team it was becomes organization-wide, which can make it visible to more people.
The team, its memberships, its place on every resource, and any identity-provider link go in one step, so a half-deleted team cannot remain. A conversation loses its team assignment; if no person is assigned either, it returns to administrator triage. Imported-file configurations also lose that team scope. Deleting a team does not delete its members' accounts.
Teams synchronized through [enterprise SSO or SCIM](https://docs.tale.dev/platform/admin/enterprise-sso) also depend on the identity provider's provisioning rules. Check that source before making a local change you expect to persist.
# Two-factor authentication
Source: https://docs.tale.dev/platform/admin/two-factor-authentication
Protect your account with an authenticator app or a passkey. Members set up their own sign-in methods in **Settings > Account**; admins can require a second factor and help a member recover access.
## Choose a sign-in method
| Method | What you need | How you use it |
| --- | --- | --- |
| Authenticator app | A Tale password and an app that supports time-based codes (TOTP) | Enter your password, then the app's six-digit code. |
| Passkey | A compatible device or security key | Approve the browser's sign-in prompt with your device or key. You can also use it after a password sign-in. |
| Backup code | A saved code from authenticator setup | Use it once in place of an authenticator code when you cannot access the app. |
A passkey satisfies Tale's two-factor policy even if you have never set up an authenticator. Accounts that sign in only through SSO do not show authenticator setup because it requires a Tale password; your organization's SSO exemption determines whether you need a Tale passkey.

## Set up an authenticator
1. Open **Settings > Account**, find **Two-factor authentication**, and select **Enable two-factor**.
2. Enter your current Tale password and select **Confirm**. A wrong password counts toward the same temporary lock as a failed sign-in.
3. Scan the QR code with your authenticator app. If scanning is unavailable, enter the displayed setup secret manually in the app.
4. Enter its current six-digit code in **Verification code**, then select **Verify and enable**.
5. Download or copy the backup codes before selecting **Done**. Tale does not display them again.
The account page now confirms that two-factor authentication is active. At your next password sign-in, enter a code from the same authenticator entry.
Keep recovery codes somewhere you can reach without the device you use to sign in, such as a password manager available on another trusted device.
## Add a passkey
1. Open **Settings > Account**, find **Passkeys**, and select **Add a passkey**.
2. If you signed in more than a day ago, Tale first asks for your password. Enter it in **Password** and select **Confirm**. A wrong password counts toward the same temporary lock as a failed sign-in.
3. Give **Passkey name** a recognizable name, such as `Work laptop`.
4. Leave **Authenticator type** on **Any (recommended)** to see the browser's available options, or choose the built-in device authenticator or a security key/phone.
5. Select **Add a passkey** and complete the browser prompt.
The passkey appears in your account's list. On the sign-in page, choose **Sign in with a passkey**. After a password sign-in, **Use a passkey instead** is also available on the verification screen.
To stop using a passkey, select its **Remove** button and confirm. If it was your only second factor and the organization requires one, you must set up another.
## Recover access and replace codes
On the verification screen, select **Use a backup code instead** and enter one saved code. Each code works once. After signing in, open **Settings > Account** and select **Regenerate backup codes** if you need a fresh set; confirm your password and save the new codes. This invalidates every previous code, including unused ones.
If a code is rejected, check that you selected the authenticator entry for this Tale account, use the current code, and check the device's clock. Repeated failures can temporarily block verification; follow the message shown instead of repeatedly submitting.
If you have no usable authenticator, passkey, or backup code, contact an organization admin. Do not send them your password, setup secret, or remaining codes.
## Require a second factor for the organization
Admins configure the policy in **Settings > Governance > Security**. Arrange a recovery contact and give members time to set up a method before enabling it.

| Setting | Effect |
| --- | --- |
| **Require two-factor authentication** | Enables the requirement after confirmation. A registered passkey or authenticator satisfies it. |
| **Grace period (days)** | Time to enroll, counted from the member's first sign-in under the policy. Zero requires enrollment immediately. |
| **Exempt SSO-only users** | Lets members without a Tale password rely on their identity provider's authentication. |
During the grace period, members see a reminder. After it expires, Tale blocks organization access until they enroll: the enrollment page asks for their password to set up an authenticator, or offers **Register a passkey instead**. A member who has been signed in for more than a day confirms their password before the passkey prompt; one who signs in only through SSO selects **Sign in again** first. Disabling your personal authenticator does not exempt you from this policy.
## Help a locked-out member
Verify the person's identity through your organization's recovery process first. Then open **Settings > Members**, edit the member, and select **Reset two-factor**. Confirming clears their authenticator setup and ends all active sessions. They can sign in again and set up a new authenticator; an enforced policy requires enrollment before they continue.
For a lost passkey, remove that credential in the member dialog's **Passkeys** section instead. Admin removal also ends all of that member's sessions. Review recovery actions in [audit logs](https://docs.tale.dev/platform/admin/governance/audit-logs).
# Understand project agents
Source: https://docs.tale.dev/platform/agents/concepts
A project agent is a named worker for tasks in one project. You configure how it runs and what it may use, then give it a task with a reviewable outcome. It can work on files and commands in a sandbox. The [configured reviewer](https://docs.tale.dev/platform/projects/tasks#review-default), a person or an independent project agent, checks the result before completing the task. New reviews that require human independence or competence records go to the human review chain; a captured agent review needs explicit transfer to an eligible person.
## Choose the right working mode
| Use | Suitable work | What you configure |
| --- | --- | --- |
| Chat | Ask a question, retrieve knowledge or draft text in a conversation. | The message, model and optional project context. |
| Project agent | Review a repository, prepare files or carry out a task over several turns. | A reusable worker in the project. |
| Automation | Run defined steps, react to events or wait for an approval between actions. | A versioned workflow and its inputs. |
A project chat still uses the built-in chat assistant. Adding a project to a chat does not select one of the project’s agents. An automation’s agent node has its own configuration.
## Give the agent a clear responsibility
Start with a responsibility you can evaluate, such as “Review changes for regressions and report evidence.” Keep that in the agent’s standing instructions. Put the particular repository, files, acceptance criteria and deadline in each task.
An agent belongs to exactly one project. People who can read the project can see its agents; people with project edit access can manage them while the project is active. Names must be unique within the project, and a project can contain up to 50 agents. Another project needs its own configuration even if it uses the same name and instructions. A project without agents of its own isn't left without one: unless an admin has turned it off, the organization's [standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent) takes its tasks, with settings an admin chooses for the whole organization.
## Understand the configuration
| Part | What it controls | Example decision |
| --- | --- | --- |
| Harness | The coding program that runs the session in a sandbox. | Choose a runtime supported by the available credential. |
| Model and provider | The model called and the provider serving it. | Select the provider/model pair approved for the work. |
| Instructions | The agent’s reusable responsibility and working rules, up to 20,000 characters. | Require evidence and a report of checks performed. |
| Skills | Instruction bundles and supporting files. | Add the team’s review checklist. |
| Connectors and tools | Connected services and allowed platform operations. | Grant repository access and only the task tools needed. |
| Secrets | Named organization credentials supplied to the running session. | Use a narrowly scoped token for a service without a connector. |
The skills, connectors, tools and secret-name lists each allow up to 25 entries. A grant to a write tool authorizes its supported writes within its access rules; an instruction asking the agent to be careful does not remove that permission. Only an Owner or Admin may change secret grants.
```mermaid
flowchart LR
P[Project task and acceptance criteria] --> A[Configured agent]
H[Harness and model] --> A
I[Standing instructions] --> A
E[Skills, connectors, tools and secrets] --> A
A --> R[Report and files for review]
```
## Check readiness before assigning work
The provider credential must support the selected harness and model, and sandbox capacity must be available. Success in ordinary Chat proves neither condition. A task should explain what success looks like and include the material the agent needs to inspect.
[Create a project agent](https://docs.tale.dev/platform/projects/project-agents) once those choices are clear. [Task automation](https://docs.tale.dev/platform/projects/task-automation) explains starting, steering and reviewing its work.
# Choose an agent runtime
Source: https://docs.tale.dev/platform/agents/harnesses
A harness is the coding program that runs an agent’s session in a sandbox. It asks the model what to do, reads and writes files, runs commands, and returns a report. Choose it for project-agent tasks or an automation’s `agent` node; the ordinary Chat model picker does not select a harness.
## Select the runtime and check access
In a project’s **Agents** tab, open an agent and choose **Agent type**. An automation’s agent node calls the field **Harness**. Then choose the model and provider. Under **Settings > AI providers**, **Agent runtimes** shows which execution paths the organization can currently serve.
A runtime needs both compatible credentials and [sandbox capacity](https://docs.tale.dev/platform/admin/sandboxes). A working chat model is not enough. If a runtime is missing or has no models, inspect its status and provider credential before changing the task prompt. A model whose tool calls need OpenAI’s Responses API, such as GPT-6 Astra or GPT-6.1 Sol, runs only on Codex, so the model picker offers it for no other runtime.
## Compare supported runtimes
“Managed” means the runtime calls through Tale’s model gateway. “Direct” means the session receives a credential for the provider’s own tooling. The built-in definitions support these combinations; your deployment and credentials determine what is available.
| Harness | Credential path | Receives new instructions in the running process | Tale’s MCP channel |
| --- | --- | --- | --- |
| Claude Code | Managed or direct | Yes | Yes |
| Codex | Managed or direct | No | Yes |
| Cursor | Direct only | No | No |
| Gemini CLI | Managed or direct | No | Yes |
| Hermes | Managed or direct | No | No |
| OpenClaw | Managed or direct | No | Yes |
| OpenCode | Managed only | No | Yes |
| Pi | Managed or direct | No | No |
| Qwen Code | Managed or direct | No | Yes |
**Claude Code (compact prompt)** is an optional choice for focused workflows that already supply complete task instructions. It shortens the runtime’s built-in instructions and some tool descriptions while retaining tools, hooks, MCP and Tale’s added guidance. Check task results and elapsed time with your model before adopting it; a smaller prompt does not guarantee a faster run. Choose ordinary **Claude Code** to restore its full prompt on the next fresh run. A provider subscription restricted to Claude Code does not automatically support the compact choice.
To guide project work, comment on the task and mention its agent. Claude Code receives that guidance at a tool boundary. For the other runtimes, Tale stops the current process and continues the conversation in a new process with the comment. This is why a running task may restart its process after you give it direction.
Gemini CLI is the exception: Tale never continues one of its conversations. A later comment, an automatic retry, or an answer to a question it asked starts a fresh conversation over the same workspace, with the task brief and the earlier rounds restated, because the runtime cannot resume a conversation in which it ran a tool. Its files and output are preserved; only the conversation starts over.
## Understand credential exposure and cost
With a stored API key or deployment-environment credential, Tale supplies a session-scoped gateway key. The original model-provider key stays at the platform. Calls through this gateway are metered and subject to the applicable spending rules, including allowance already assigned to other running turns.
Vendor subscriptions use their supported harness and receive the subscription credential in the session environment. They cannot be used as ordinary chat credentials or with an incompatible harness, because the vendors permit subscription tokens only in their own runtime; [AI providers](https://docs.tale.dev/platform/admin/providers#use-subscriptions-in-tasks-not-in-chat) explains this. Their direct provider calls bypass Tale’s gateway metering and spending caps; review usage with the subscription provider.
These model-credential rules do not mean the sandbox contains no secrets. Explicitly granted **Secrets**, and the token supplied for an equipped GitHub connection, can be available inside it. Grant only the access the task needs.
## Understand files and connected tools
A project agent reuses a persistent workspace across its tasks. Task attachments are available read-only under `/agent/inputs//attachments/`. Files written to `/agent/output//` are collected as task **Deliverables** when the turn ends. Automation agent output is collected from `/agent/output/`.
Equipped skill bundles are staged as files and named in the run’s instructions. Review their instructions and scripts before granting them; [Skills on agents](https://docs.tale.dev/platform/agents/skills) explains staging and visibility.
Every runtime also finds Tale’s built-in `visual-aspect-analyzer` skill among its own skills, without equipping it. It drives a real browser over a finished UI change and reports layout shifts, flicker and other visual regressions. A skill of the same name in the workspace repository’s `.claude/skills` and `.agents/skills` folders takes its place in every runtime that reads a repository’s skills.
Tale’s connector broker keeps ordinary connector credentials at the platform and returns action results. It exposes read actions to agents, and refuses writes through that broker. Use an automation connector node for a governed connector write. GitHub tooling and explicitly granted secrets have their own access paths, so the broker’s read-only rule is not a general ban on all shell writes.
Agents with Tale’s MCP channel can also look up which Tale release the platform reports. Along with the platform tools granted to the agent, `workspace_status` returns `platform.version`, the release number that the answering backend’s build is labelled with: for example `0.5.64` for the release whose tag is `v0.5.64`. The backend reads nothing from the request for this value. A build without a release number, such as a development build, returns `null` with a short note instead. Treat the value as a label rather than proof, because any build can carry a release number. It also describes that backend only and does not show whether the rest of the deployment is healthy; the deployment’s [status page](https://docs.tale.dev/develop/status-page) reports on its services.
Outbound access normally permits package installation and repository cloning while blocking private addresses and cloud metadata targets. Operators can restrict permitted hosts further. If a command cannot reach a site, check the network policy instead of assuming the credential is wrong.
Tale’s built-in document skills `docx`, `pptx`, `xlsx` and `pdf` find the libraries they call already installed in the sandbox, so an agent equipped with them creates and reads Word, PowerPoint, Excel and PDF files even where package installation is blocked. Their instructions still include install commands such as `npm install -g docx`; where the registry is blocked, that step fails while the preinstalled library stays available. Text recognition (OCR) for scanned PDFs is not included.
## Check the result
The runtime determines when its turn is finished; Tale collects the report and output. Read both before marking the task complete. Confirm which checks actually ran and which depend on services unavailable in the sandbox. [Task automation](https://docs.tale.dev/platform/projects/task-automation) explains the review loop; [execution logs](https://docs.tale.dev/platform/automations/execution-logs) explains an automation’s agent-step result.
If the runtime cannot start at all — a configuration it refuses, a state directory it cannot find — the run fails at once and its reason quotes the last lines the runtime wrote, so the cause is named rather than a bare exit code. Such a run is not retried automatically; fix the cause, then retry.
# Equip agents with skills
Source: https://docs.tale.dev/platform/agents/skills
Equip an agent with a skill when it needs a reusable procedure or reference material from the organization's [skill library](https://docs.tale.dev/platform/workspace/skills). The library stores the bundle; the agent's equipment determines which bundles are available to its runs.
## Choose a skill for the task
A useful skill explains when to use it, how to perform the work, and what a good result looks like. For example, equip a release-note skill with the agent that prepares releases, then give it a small set of changes and check its output against your expected format.
The bundle contains `SKILL.md` and may include references, assets, or scripts. Importing it does not execute those files. Once equipped, its instructions can guide a coding agent that has a shell or other tools, including running a bundled script. Review the whole bundle before use; a skill does not add a separate permission boundary.
## Equip a project agent
Open the [project agent](https://docs.tale.dev/platform/projects/project-agents) and select the needed skills in its equipment. The available list follows the project's access, even if you personally can read more skills:
| Project scope | Available skills |
| --- | --- |
| Organization-wide project | Organization skills |
| Project shared with teams | Organization skills and team skills shared with at least one of the project's teams |
Legacy private skills cannot be equipped on a project agent. The same access rule is checked when a task runs; selecting a skill does not grant the project permanent access to it.
A new project agent starts with the document skills `docx`, `pptx`, `xlsx` and `pdf` ticked when the project can access them. Untick those the job does not need.
Each skill in the list names who created it: a member, **Built-in** for the skills your organization starts with, or **Configuration release** with the member whose upload installed it. Check where an unfamiliar skill comes from before you equip it.
## Use skills in an automation
An automation's agent nodes declare the skills they need. A run bound to a project uses that project's scope. An organization-level run can use organization skills only. Your personal membership in additional teams does not expand either scope.
During sandbox setup, Tale stages the equipped bundles as files and lists each skill with a description excerpt of up to 300 characters and the path to its `SKILL.md` instructions. The excerpt helps the agent choose a skill for a plain-language task; it is a selection hint, not an instruction to execute. Supporting files are available alongside the instructions.
For a skill marked `disable-model-invocation`, the list tells the agent to wait until the task asks for that skill by name. This is guidance, not an access control. Keep the equipment focused and name the procedure when it matters; availability alone does not prove that the result followed it.
## Resolve missing or changed skills
Deleting a skill unequips it from every agent that carried it, and the audit log records which ones. If a required skill is no longer shared with the run's scope, staging fails and names the unavailable skill; the run is not retried automatically, because nothing about a retry changes that. In the agent's dialog the skill is listed as unavailable so you can untick it — every other setting of the agent still saves. Restore the intended access or remove the obsolete equipment before retrying.
Changes to a shared bundle affect later staging for its users. Review replacements and test an agent with a known input after a substantial change. Do not assume a repository's similarly named skill overrides the equipped bundle.
## Choose skills or agent instructions
| Put it in a skill when… | Put it in agent instructions when… |
| --- | --- |
| Several agents share the procedure. | It defines this agent's role or voice. |
| The procedure needs reference files or scripts. | It is a short, stable rule for this agent. |
| Maintainers should update the procedure in one place. | It describes how this agent should use its equipped skills. |
Use the [skill library guide](https://docs.tale.dev/platform/workspace/skills) to create, import, edit, and share a bundle.
# Understand operation approvals
Source: https://docs.tale.dev/platform/approvals/concepts
An operation approval lets a person review a proposed connector write before it happens. For example, an automation can prepare an email, then wait for you to check its recipient and body before sending it.
## When a write waits
A live automation pauses when it reaches a connector write that the organization's policy requires someone to approve. By default, writes to external systems need approval; writes through internal, platform-authenticated connectors do not. The organization can change that rule for a connector or a specific action. See [Configure approvals](https://docs.tale.dev/platform/approvals/configure).
Reads do not request operation approval. **Test run** uses mock connectors, so it does not perform the external write or show its live approval card. Passing a test does not prove that the proposed live action is appropriate.
## Review the proposed operation
Open the automation's [run list](https://docs.tale.dev/platform/automations/execution-logs) and select the run marked **Waiting**. Its approval card names the operation, such as `imap-smtp.send`, and the node requesting it. **The step would call with** shows the exact input.
Check the destination, recipients, content, and identifiers against the intended task. Review sensitive details in the input before choosing:
- **Approve** allows this operation to execute when the run resumes.
- **Reject** prevents this operation and causes the step and run to fail.
The card does not edit the operation. If an input is wrong, reject it, correct the workflow or its input, and start a new run.
Organization members can decide connector operation approvals. These cards do not route to a named reviewer or approver group; the decision is made on the run detail, which only Owners, Admins, and Developers open. When the run works a task, its card also appears on the task, where anyone who can open the task decides it. Other kinds of review can have stricter permissions.
## Check what happened next
Approval permits execution; it does not guarantee that the connector succeeds. Check the resumed run's status, node result, and effects. A rejected run shows the rejection as its failure reason. The [audit log](https://docs.tale.dev/platform/admin/governance/audit-logs) records the decision and actor.
An existing pending approval stays pending if the policy is relaxed. The same operation in the same run keeps its recorded decision; starting a new run creates a new execution that is evaluated again. A finished or cancelled run cannot use an outstanding approval to perform its write.
## Distinguish approval from a question
An agent node may also pause because it needs information from a person. Answering that question supplies input; it is not approval of a connector write. [Approvals in workflows](https://docs.tale.dev/platform/automations/approvals-in-workflows) explains both interactions. Task reviews, controlled document reviews, and erasure requests have their own [review rules](https://docs.tale.dev/platform/approvals/configure#distinguish-the-other-human-decisions).
# Decide which actions need approval
Source: https://docs.tale.dev/platform/approvals/configure
An approval policy decides which connector writes must wait for a person during a live automation run. Review the workflow’s outbound actions before deploying it, especially when it sends messages or changes another system. [Approval concepts](https://docs.tale.dev/platform/approvals/concepts) explains the decision card itself.
## Start with the default behavior
Reads do not require operation approval. By default, writes to external systems, such as sending mail, posting to Slack, creating a GitHub issue, or writing to WebDAV, pause for approval. Internal operations, such as updating a task or saving a document in Tale, do not ask by default.
An allowed operation still runs under the relevant access rules. A missing approval card is not evidence that an operation is read-only: it may be an internal write or one that your organization explicitly auto-approves.
## Request a policy change
There is no per-action approval switch in the connector settings page. The organization’s approval policy can require or bypass approval for a connector or an individual action. A rule for one action takes precedence over a rule for its connector.
Ask the person managing your deployment to apply the [approval policy configuration](https://docs.tale.dev/self-hosted/configuration/approvals). Name the exact operation, why it should ask or proceed automatically, and which workflow uses it. Cloud administrators should coordinate the change with whoever manages their deployment.
An operation already waiting for approval keeps its pending decision after a policy change. Approve or reject that card explicitly; loosening the policy does not release it.
## Check a workflow before it goes live
1. Inspect each connector node and identify whether it reads or writes.
2. Confirm which writes the organization’s effective policy auto-approves.
3. Run **Test run** to check inputs and output against mocks.
4. During a controlled live run, inspect the operation and exact input on any pending card before deciding.
A mock test does not prove that a live approval will appear. Mock connectors do not change external systems and do not request approval.
## Distinguish the other human decisions
| Decision | Where to read the rules |
| --- | --- |
| Accept an agent’s task result | [Task automation](https://docs.tale.dev/platform/projects/task-automation). A person moves the result from In review to Done. |
| Approve a controlled document version | [Documents](https://docs.tale.dev/platform/knowledge/documents). The named reviewer decides on the frozen version. |
| Authorize an erasure request | [Data subject requests](https://docs.tale.dev/platform/admin/governance/data-subject-requests). A second Admin provides the required approval. |
| Answer an agent node’s question | [Approvals in workflows](https://docs.tale.dev/platform/automations/approvals-in-workflows). The run needs information to continue. |
These decisions have their own rules; the connector approval policy does not disable them. Chat has read-only retrieval tools and does not create operation approval cards.
# Respond to a waiting workflow
Source: https://docs.tale.dev/platform/automations/approvals-in-workflows
A run can wait for a decision before writing through a connector, or for information an agent needs to continue. Open the run detail to see which response is required. A waiting run has not finished, even if earlier nodes succeeded.
## Find the waiting run
Open the automation and its [execution logs](https://docs.tale.dev/platform/automations/execution-logs), then select the run marked **Waiting**. Check the version and run input so you know which execution you are reviewing.
An operation approval names a connector action and shows its proposed input. An agent question instead asks for an answer, with choices or a text field. These are separate interactions: answering a question does not approve a later write.
## Approve or reject a write
Read the operation and **The step would call with** carefully. Check the recipient or destination, the content, and any identifier that selects what will change.
Choose **Approve** to allow the operation. The run resumes and attempts the write; check the node result and effects afterwards. Choose **Reject** if the request is wrong or should not happen. Rejection prevents that operation and fails the run.
The card keeps telling you where the run stands when something goes wrong. If the approval cannot be loaded, the card says so and offers **Try again**; the run keeps waiting until someone decides. If your choice was not recorded, the card says so and keeps **Approve** and **Reject** available, so you can choose again. If someone else decided first, the card shows the decision on record.
A live run checks that the connector has a usable credential before it asks for approval: when none is configured, the node fails with that reason instead of waiting for a decision.
You cannot revise parameters on the approval card. Reject an incorrect request, fix the workflow or run input, and test the correction before starting a new live run. Changes to the approval policy do not release an already pending card. See [Approval concepts](https://docs.tale.dev/platform/approvals/concepts) for the decision lifecycle and [approval policy configuration](https://docs.tale.dev/self-hosted/configuration/approvals) for operator rules.
## Answer an agent’s question
When an agent node uses `ask_human`, the run detail displays **The agent needs your answer to continue**. If choices are offered, answer the questions on the card. For an open question, enter text under **Your answer**, then choose **Send answer & resume**.
Give the missing information directly. If the agent asks which document to use, name the document or provide its identifier rather than a general instruction to continue. The waiting node resumes with your answer, and the run may later need another answer or an operation approval. Owners, Admins, and Developers answer on the run detail. When the run works a task, the question also appears on the task, where anyone who can open the task answers it.
## Correct and test the workflow
Changing a workflow definition is separate from responding to its current run. Save a corrected version in the [workflow editor](https://docs.tale.dev/platform/automations/editor), test it against mocks, then deploy it when it is ready for live use. Saving a version does not revise a call already waiting for approval.

A mock test does not perform external connector writes or request their live approvals. For a practical walkthrough, follow [Build a workflow with approvals](https://docs.tale.dev/tutorials/editor/workflow-with-approvals). After a live decision, inspect both the run outcome and the [audit log](https://docs.tale.dev/platform/admin/governance/audit-logs); permission to proceed and successful execution are different results.
# Choose how to author an automation
Source: https://docs.tale.dev/platform/automations/assistant
Edit an automation directly on its canvas, or connect an external assistant through MCP to author it with Tale’s tools. Both routes save versions of the same workflow and use the same validation and deployment rules. You need Developer-level permission to author and deploy automations.
## Make a change in the visual editor
Open **Automations** and select the workflow. Select a node to inspect its input, model, code, or other configuration. Save the change with a version message, run a test, and deploy the intended version when its checks pass.

[The workflow editor](https://docs.tale.dev/platform/automations/editor) covers these steps in detail, including inspecting a run and returning to an earlier version. The canvas does not include a conversational assistant panel.
## Use an external assistant through MCP
Configure your client with the endpoint under **Settings > API > MCP** and an appropriate organization API key. Give it the desired inputs, output, and the systems the workflow may change. Ask it to inspect existing automations and available capabilities before creating another one.
The [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) provides documentation, validation, save, test, and deploy tools. Review the resulting workflow and its test output before deployment. A successful save creates a version; it does not make that version live.
## Keep runtime decisions separate
An agent node inside an automation performs work during a run. It is separate from the client you use to write the workflow. Similarly, an [approval](https://docs.tale.dev/platform/approvals/concepts) authorizes one pending operation during execution; it does not approve a proposed edit to the workflow definition.
Choose [the editor](https://docs.tale.dev/platform/automations/editor) for a change you want to make directly, or [MCP](https://docs.tale.dev/develop/mcp-endpoint) for authoring from your own client. Start from [an existing automation](https://docs.tale.dev/platform/automations/catalog) when a suitable one is already available.
# Built-in automations
Source: https://docs.tale.dev/platform/automations/builtin
Tale includes ten automation packages: three mailbox syncs, three inbox digests, two GitHub review workflows, and GitHub and GlitchTip issue imports. Each starts as version 1 and **Not deployed**. The issue importers run manually; the other packages include schedules. Inspect their inputs, model, connections, and writes before an Owner, Admin, or Developer deploys a version.

## Start with one package
Open **Automations**, select a package, and inspect its nodes in the [workflow editor](https://docs.tale.dev/platform/automations/editor). Check that the required connector is connected and that the model used by any `llm` node is one your organization serves — the packages name a model your providers may not offer, and validation warns about it when you save; pick a served model in the node’s **Model** field before a live run. A test run uses mock responses; it validates the flow without proving access to your real mailbox or repository.
The packages are added when an organization is created. Existing versions are preserved when the shipped package changes; only its shipped name and description refresh. A deleted package stays deleted. Your edits create new versions, which you deploy separately.
## Sync mail into the Inbox
These workflows pull new messages into conversations every five minutes. Each declares the **Inbox** view: deploying one adds that view to [Home](https://docs.tale.dev/platform#home) and offers its connected mailbox in the compose form. Before deployment, Home has no **Inbox** view. A link to the inbox shows a setup notice instead: for Owners, Admins, and Developers it points to **Automations**; everyone else is told that one of those roles must deploy an email automation.
| Automation | Required connector | Schedule |
| --- | --- | --- |
| Sync Gmail emails | Gmail | Every 5 minutes |
| Sync Outlook emails | Outlook | Every 5 minutes |
| Sync emails via SMTP/IMAP | IMAP/SMTP | Every 5 minutes |
A pass reads the mailbox's Inbox folder only. It tells the mailbox's own replies from a customer's by the mailbox address, which the first pass learns from the connected account and keeps on the connection as its From address; IMAP takes it from the login.
Connect the matching mailbox first. After the first live run, inspect its [execution log](https://docs.tale.dev/platform/automations/execution-logs) and check that the expected messages appear in the **Inbox** view in Home.
Attachments are stored with their message, so you open and download them from the conversation. Gmail hands over each attachment only up to 3.5 MB: a larger one is still listed with its name and size, but you open it in Gmail.
## Triage the Inbox
These workflows work on the **Inbox** that the sync automations fill, every six hours. Each one reads the open conversations of its mail connector whose newest message is the customer's and that no earlier pass has judged since that message arrived — 25 per run, or the `limit` you set, up to 100. The model decides for each thread whether a person has to reply, and how urgent it is.
What a run changes:
- The verdict is recorded on each thread, with its reason, so the next run looks at the thread again only when the customer writes again.
- The thread's **priority** is set to the model's verdict — but only when nobody has set one. A priority a person chose stays as it is.
- Where a reply is due, a second model call drafts one and leaves it on the thread as a **Suggested reply**: a card above the composer, apart from anything a person typed. **Put in editor** hands the text to the composer to edit and send; **Discard** drops it, and the thread is not proposed on again until the customer writes again. Nothing is sent by the automation. A thread that already carries a suggestion keeps it.
The run's output lists what was read, the model's summary, the threads that need a reply with their Inbox links, and the drafts it left. A run with nothing waiting makes no model call. Deploy the matching sync automation first; without synchronized conversations there is nothing to triage.
| Automation | Required connector | Schedule |
| --- | --- | --- |
| Triage the Gmail inbox | Gmail | Every 6 hours |
| Triage the Outlook inbox | Outlook | Every 6 hours |
| Triage the IMAP inbox | IMAP/SMTP | Every 6 hours |
## Import and synchronize issues
**Import GitHub issues** and **Import GlitchTip issues** use the same form to turn source issues into tasks. They start without a schedule. These imports read the source and write Tale tasks; they never comment on, close, or change the upstream issue and do not start an agent.
1. Connect the source under **Settings > Connectors** and choose its default credential. GitHub needs repository access with read permission for issues. GlitchTip needs its instance URL and a token with `project:read` and `event:read`; a project-provisioning token alone cannot read issues. Self-hosted instances must be allowed by the deployment's connector host policy.
2. Open the importer and choose **Test run**. Select the **Tale project**, then enter the GitHub owner and repository or the GlitchTip organization and project slugs. Optional labels or a GlitchTip search narrow new issue discovery. **Maximum issues** accepts 1–500, defaulting to 100.
3. Inspect the test result, deploy the version, and choose **Run live** with the same destination and filters. A test run uses fixtures and creates no tasks; only a live run verifies the real connection.
4. Open **Runs** and select the run. **Imported tasks** links to the corresponding Tale tasks. If another batch remains, **Continue import** carries the same source, destination, and continuation position into the next run. To import every issue on a schedule instead, an automation keeps that position between its occurrences: see [Import every issue on a schedule](https://docs.tale.dev/platform/automations/triggers#import-every-issue-on-a-schedule).
Each synchronization discovers new issues and refreshes up to 500 linked issues, prioritizing the oldest checks. Linked issues are refreshed even when they no longer match the discovery filter. Run again to keep large collections current. GitHub pull requests are excluded. Repeating or retrying an import reuses the same source identity within a Tale project; repository and project renames update the canonical source link instead of creating a second task. Issues moved to another repository or source project remain linked and continue to refresh through earlier imports, provided the connection can read their new location.
A task's source card shows the latest source title, description and status separately. When it imports an issue, Tale cuts a title over 200 UTF-16 code units or a description over 20,000 (most emoji count as 2) to that length on the task, ending it in "…"; the source card and the linked issue keep the full text. Closing or resolving a source issue leaves the Tale task's status, title, description, assignee and priority unchanged. A source issue that becomes unavailable retains its last known snapshot; authentication and rate-limit errors fail the run rather than declaring the issue deleted. Review and complete work in Tale as usual.
## Review GitHub work
**Triage GitHub issues** reads open issues, scores whether they are actionable and their priority, and returns a ranked shortlist with reasons. It does not write to GitHub or create project tasks. Its default limit is 50 issues per run.
**Review GitHub pull requests** reads open pull-request diffs and posts findings as review comments. Its default limit is 10 pull requests per run. It does not approve or merge a pull request. Review the target repository before running it live, because a repeat run can add another comment.
| Automation | Required connector | Shipped schedule | Writes |
| --- | --- | --- | --- |
| Triage GitHub issues | GitHub | Daily at 07:00 UTC | None; read the run output |
| Review GitHub pull requests | GitHub | Every 30 minutes | A review comment per processed pull request |
Both workflows require `owner` and `repo`. In **Test run**, supply **Run input (JSON)** using your repository’s values:
```json
{
"owner": "your-organization",
"repo": "your-repository",
"limit": 5
}
```
The shipped GitHub schedules do not supply `owner` and `repo`; deploying alone does not make those scheduled runs valid. A schedule sends only `trigger` and `firedAt`, so the required repository input is missing and the start is refused. Run manually with the required input, or adapt the workflow’s schema and repository configuration before enabling scheduled execution. A refused scheduled start appears as `start_refused` on the [trigger](https://docs.tale.dev/platform/automations/triggers).
Before deploying, read the resolved input and output of the test run. For a live run, also check connector permissions and any approval requirements. [Execution logs](https://docs.tale.dev/platform/automations/execution-logs) explains waiting, failure, and the recorded writes.
# Create or import an automation
Source: https://docs.tale.dev/platform/automations/catalog
Open **Automations** to find the workflows available in your organization. Only the Owner, Admin and Developer roles see this section and manage its automations. Start with a [built-in automation](https://docs.tale.dev/platform/automations/builtin) when it matches your task, or create a draft you can test before making it live.
Search by name or slug. For example, enter `Triage` to compare the shipped triage workflows.

## Choose a starting point
Each row shows the automation’s name, project bindings, version count and deployed version, or **Not deployed**. Open it on the **Editor** tab to inspect the workflow. **General** holds its trigger and its projects: **Projects** controls which boards can use it; without project bindings, it serves the organization. **Projects** offers only the projects you can open, and saving keeps any binding to a project you cannot see. An automation bound only to such projects does not appear in your list. The **Version** selector beside the tabs shows its saved history; **Runs** shows recent executions.
The **Create automation** menu offers two routes:
| Choice | Use it when | What happens next |
| --- | --- | --- |
| **Blank (trigger + agent)** | You want to configure the workflow yourself. | Set the name, model, instructions and equipment, then choose when it runs. Creation opens the editor for further changes. |
| **Upload package** | You already have a workflow file or a reusable pack. | Tale validates the files and saves a draft version. |
Shipped automations are already installed when the organization is created. They still need configuration and a deployed version before automatic use. Follow [the workflow editor](https://docs.tale.dev/platform/automations/editor) to test inputs, inspect results and deploy deliberately.
## Import a package
A pack contains the required `workflow.yml`, an optional `automation.yml` manifest and, optionally, skill bundles:
```text
review-invoices/
├── workflow.yml
├── automation.yml
└── skills/
└── invoice-rules/
├── SKILL.md
└── references/
└── checklist-rules.md
```
Choose **Create automation > Upload package**. Upload the workflow and optional manifest as individual files, or select one `.zip` containing the pack. Use a zip when carrying skills; upload that archive on its own. Markdown notes outside `skills/`, dotfiles and build leftovers such as `node_modules/` and `__pycache__/` are ignored.
Under **Install into**, choose **Organization** or an existing project. A manifest declaring `scope: project` requires a project. Installing an existing automation into another project adds that binding; it does not remove previous ones. You can adjust the full set later under **Projects** on the automation's **General** tab.

Choose **Upload package** and resolve any reported document, manifest or skill issues. Validation completes before the upload writes the automation and carried skills. A successful upload creates a draft; uploading the same automation again appends a version and preserves its history.
Choose **Later** to inspect and test the draft in the editor. The success dialog also offers deployment of the numbered version. Uploading alone does not change the live version. Configure required credentials and check the supplied skills before deploying.
Keep a zip within 20 MiB compressed and 20 MiB expanded, with at most 500 files, 2 MiB per file and 20 skill bundles. If rejected for size, remove generated artifacts and split unrelated material into separate skills rather than increasing compression alone.
## Resolve skill conflicts
The manifest’s `skills` list must match the folders carried under `skills/`: undeclared folders and declared-but-missing bundles are rejected. Each bundle needs valid `SKILL.md` frontmatter with a `name` matching its folder. Carried skills follow the skill library's rules: a new skill belongs to the person uploading the package, and a replacement keeps its owner or belongs to the uploader if it had none. New or changed team lists may name only teams the uploader may share with; existing lists can stay unchanged. Tale checks every carried skill's audience before installing any of them.
```yaml
# automation.yml
name: Review invoices
skills:
- invoice-rules
```
New bundles are installed into the organization’s [skill library](https://docs.tale.dev/platform/workspace/skills), and identical bundles remain unchanged. Different content pauses the upload and lists the affected slugs. Confirm replacement only after reviewing them: the package replaces those shared bundles, and the previous `SKILL.md` remains in each skill’s history. No automation or skill is written before that confirmation.
A workflow may also refer to library skills it does not carry. If one is missing, the upload reports a warning; install an accessible bundle before running the agent that needs it. A saved draft does not prove all its dependencies are ready.
## Configure a project through package forms
A manifest can declare forms that appear when someone selects the automation’s task template. Values belong to the project, so two projects can use the same automation with different policies.
```yaml
# automation.yml
settings:
folder: Setup
forms:
- file: validation-policy.yaml
title: Validation policy
required: true
fields:
- key: method
label: Validation profile
type: select
default: strict_rules
options:
- value: strict_rules
label: Strict checklist
```
A required form appears before the task’s own fields if that project has not been configured. **Save and continue** writes the forms and proceeds to task creation. Later, **Settings** reopens them as tabs; a dot marks unsaved changes, and **Save** writes every changed form. Closing with unsaved edits asks for confirmation.
Saving replaces the form’s flat YAML file, such as `Setup/validation-policy.yaml`. Existing values prefill the form, including values uploaded by hand. Supported field types are `text`, `number`, `boolean` and `select`; stored values are strings. Text fields can specify a `pattern`, and per-entry `i18n` blocks localize titles, labels, help and options. Keep nested structures and lists in separate files the workflow reads.
## Supply reference files through an upload form
An upload form manages files directly instead of producing YAML:
```yaml
settings:
folder: Setup
forms:
- kind: uploads
title: Reference documents
subdir: reference
accept: ['.pdf', '.json']
match: '\.(pdf|json)$'
requireFolder: true
```
`subdir` chooses a subfolder of the settings folder. `accept` limits extensions offered by the picker; `match` filters listed names case-insensitively and rejects uploads that would not appear. With `requireFolder: true`, select or create a subfolder before uploading, for example one per reporting period.
Uploads apply immediately and have no **Save** action. They never block task creation. Runs read the folder’s current contents, so finish preparing the reference set before starting work that depends on it.
## Define what the reviewer should receive
The manifest can name deliverables in the task’s **Outcome** area. Declared files stay in the specified order; other attachments and working files remain under **Files**.
```yaml
subjects:
task:
outcome:
files:
- return.xml
- report.md
- name: audit-summary.md
optional: true
```
A required file appears as **Not ready yet** until a run files it. An optional file appears only once it exists. Patterns support `*` and `?`, such as `return-*.xml`. Without declarations, the outcome shows all files filed by runs, newest first. Use a short explicit list when the reviewer needs to distinguish the final report from supporting work.
## Ask before an approval that decides more
**Approve** closes the task in one click. When approving means more than closing the task, declare that consequence, for example when an integration reports the approval to a client as a filing. **Approve** then asks first and shows your sentence:
```yaml
subjects:
task:
review:
requestChanges: true
approve:
confirm: Approving tells the client this return has been filed with the tax authority. Approve only after you have filed it.
i18n:
de:
confirm: Mit der Freigabe erfährt der Kunde, dass diese Abrechnung bei der Steuerverwaltung eingereicht ist. Gib sie erst frei, wenn du sie eingereicht hast.
```
Without `approve`, **Approve** stays a one-click close. Translate the sentence under `i18n`; a locale without its own sentence uses its base language, then the English one.
# Automation concepts
Source: https://docs.tale.dev/platform/automations/concepts
Use an automation for work that follows a repeatable process. Its workflow describes the steps; saved versions preserve each revision, deployment chooses the version used for live runs, and a trigger can start it on a schedule or event. You can inspect each run to see its input, results, and operations.
Prefer to watch first? Episode 5 opens the triage automation end to end and decides an approval card on camera, captions included — recorded on the earlier version, where the card sat in chat; in this version it sits on the run's detail page.
## The workflow document
An automation’s `name` identifies it. Use lowercase slug segments separated by dashes; `/` groups related automations into folders, as in `billing/dunning-reminder`. The first segment must not be a reserved page name: `asks`, `builder`, `catalog`, `listing`, `metrics`, `runs`, `serving-preview`, or `upload`.
The document also contains a `description`, an `inputs` JSON Schema for the data a run receives, `nodes` for the steps, and an `output` expression for the result. Its `tests` describe examples and expected outcomes that are checked before deployment.
```yaml
name: billing/dunning-reminder
description: Remind a customer about an overdue invoice.
inputs:
type: object
properties:
invoiceId: { type: string }
required: [invoiceId]
nodes:
- id: invoice
type: transform
input:
id: '{{ input.invoiceId }}'
code: 'return { id: input.id, daysLate: 14 };'
- id: message
type: llm
model: openai/gpt-4o-mini
prompt: 'Write a polite reminder for invoice {{ nodes.invoice.output.id }}.'
output:
text: '{{ nodes.message.output.text }}'
tests:
- name: builds a reminder
input: { invoiceId: 'inv-1' }
```
The `ui` block stores canvas positions. Moving a node changes the layout, without changing its execution.
### Edges are derived, not declared
There is no edge list. One node reads another by referencing it — `{{ nodes.invoice.output.id }}` — and that reference _is_ the edge the canvas draws. Execution order is a topological sort over those derived edges, which is why deleting a reference also removes an arrow, and why two nodes that read each other are refused as a cycle.
Templates use a single `{{ }}` JavaScript-expression grammar over `input`, `nodes..output`, and, inside an iterating node, `item` and `index`.
### Control flow rides on the node
Branching and looping are fields on a node rather than separate step types, so the canvas shows them as badges on the box they affect.
| Field | What it does |
| ---------------------------- | ------------------------------------------------------------------------ |
| `when` | Run the node only when the expression is truthy; dependents skip with it |
| `elseOf` | Run exactly when the named node was skipped by its own `when` |
| `forEach` | Run once per item of a collection, with `item` and `index` in scope |
| `repeatUntil` / `maxRepeats` | Re-run until the expression is truthy, capped (default 5, maximum 20) |
| `onError` | `fail` halts the run; `continue` records the error and skips dependents |
### Node types
Four types are built in, and every connector action and platform native — knowledge search, document operations — joins the same table alongside them.
**`transform`** runs pure JavaScript to reshape data. It has no network and no imports: the body reads the node's resolved `input` and must return a value.
**`llm`** calls a language model with a templated prompt. `model` is required and always explicit — an automation never picks one on your behalf (the chat composer's Auto is a chat-only affordance). The output is `{text}`, or the schema-shaped object when the node declares an `outputSchema`.
**`agent`** runs one turn of a coding agent (Claude Code, Codex, and the other harnesses) in the sandbox. It reads staged `files`, uses `skills`, brokered `connectors`, granted platform `tools`, and injected `secrets`, and returns `{text, files, status}`; `model` is required. When an admin has turned on [image generation](https://docs.tale.dev/platform/admin/governance/content-models#let-agents-generate-images), it can also create images, which come back among its `files`. Reach for `llm` when a one-shot completion is enough, and for `agent` only when the step needs tools, files, or several turns — a live agent node runs as an asynchronous turn, so it sits at the top level rather than inside a `subautomation` and does not iterate with `forEach`.
**`subautomation`** runs another saved automation as a single node, its `automation` field naming `"name"` or `"name@version"`. Without a version it uses the deployed one, and nesting is capped at three levels.
### Structured and unstructured output
A **structured** output has named fields that you can reference with `nodes..output.`. An **unstructured** output is free text. Reference it through `nodes..output.text` in a string expression; do not treat it as an object with additional fields.
A tool without an output schema is unstructured. To turn its text into structured data for later steps, use an `llm` node with an `outputSchema`. Validation errors identify the invalid reference and the fields or context that are allowed. Correct that reference before saving again.
## Versions never change
Saving creates a new version instead of overwriting the previous one. Versions are numbered from 1 for each automation and carry the author’s change message. An existing version’s workflow stays unchanged.
A running automation keeps the version it started with, so later edits do not alter its steps. When inspecting an older run, open its recorded version to compare the input and workflow. This immutability is distinct from retention: deleting an automation or its history can remove the records.
## Deploying is a separate act
One version per automation is the deployed one, and that is the version triggers run. Promoting a version, or rolling back to an earlier one, is a single act that overwrites no history — the version list stays exactly as it was and only the pointer moves. An automation may also have no deployment at all and live purely as drafts.
A version with a failing test cannot be deployed; a version whose tests pass, or that carries no tests at all, can. Tests are stored with the document: each has a name, an input, and expectations about the output and about the effects the run should produce. Whether a version's tests passed is recorded when it is saved, so promoting reads that recorded fact instead of re-running the suite.
A live run needs a deployed version. You can still test a saved draft with **Test run** before deployment.
## What starts a run
Start a saved version manually in test mode, or run the deployed version live. For automatic starts, configure one of three trigger kinds: a schedule with a cron expression and IANA timezone, a webhook URL protected by a token, or a named platform event.
The trigger belongs to the automation’s name. Deploying another version keeps its trigger configuration and webhook URL while changing the version used by subsequent starts. Disable the trigger when you need to pause automatic runs. [Workflow triggers](https://docs.tale.dev/platform/automations/triggers) explains timing, authentication, and the input each kind supplies.
## What a run records
A run records its status (`queued`, `running`, `waiting`, `success`, `failed`, or `cancelled`), mode, starting event, input, output, and a checkpoint for each completed node. Its trace explains the steps the engine attempted.
If processing yields before the workflow finishes, the same run resumes from its checkpoints without repeating completed nodes. The effects list records connector writes; it is not a complete inventory of changes made through a sandbox or other direct tools. Run history remains subject to deletion and retention settings.
**Mock** mode simulates external operations for authoring feedback. **Live** mode can perform them and requires developer-level permissions to start. Use [Execution logs](https://docs.tale.dev/platform/automations/execution-logs) to inspect the saved version, resolved inputs, errors, and recorded effects.
## Where a human decides
An approval pauses the run in `waiting` before a protected write. Approving permits the engine to attempt that write; it does not guarantee success. Rejecting blocks it and fails the run. A question also pauses the run, but asks for information rather than permission.
A `waiting` status can also mean that an agent is still working, that an agent step waits for a sandbox slot to start, or that a node is polling a condition. Check `waitingFor`: `approval` and `ask` require a person, while `agent`, `room` and `repeat` normally resume automatically. [Approvals in workflows](https://docs.tale.dev/platform/automations/approvals-in-workflows) explains how to inspect and answer each human request.
## Choose a chat, task, or automation
| Need | Use |
| --- | --- |
| Ask a question and discuss the answer | Chat |
| Produce a reviewed result with an owner | A project task, optionally assigned to an agent |
| Run several dependent steps or react to a schedule, webhook, or event | An automation |
Check the [built-in automations](https://docs.tale.dev/platform/automations/builtin) before building. A webhook starts an automation; it is not a separate kind of project agent.
## Putting the model to work
The workflow, versions, deployment, and trigger are separate parts of one automation. Follow [The workflow editor](https://docs.tale.dev/platform/automations/editor) to test and deploy a change; use [Execution logs](https://docs.tale.dev/platform/automations/execution-logs) to inspect what happened.
# The workflow editor
Source: https://docs.tale.dev/platform/automations/editor
Use the workflow editor to change what an automation does and decide which saved version runs live. You need Developer, Admin, or Owner permissions to make changes. Saving, testing, and deployment are separate steps: editing a draft leaves the deployed version in place.
Open **Automations**, then select an automation. It opens on **Editor**. To create one first, use [Create or import an automation](https://docs.tale.dev/platform/automations/catalog).
| Tab | Use it to |
| --- | --- |
| **Editor** | Change the workflow, test a saved version and choose what runs live. |
| **General** | Choose what starts the automation and which projects can use it. |
| **Runs** | Inspect recent executions and open a run’s full record. |
The **Version** selector stays at the right of the Editor, General and Runs tabs on desktop and mobile. Open it to read version messages, dates, test results and the live marker, then select a row to open that version. On desktop, run actions sit beside the tabs together with **Save** and **Discard**; on **General**, only **Save** and **Discard** sit there. A dot on a tab marks its unsaved changes. Leaving the tab or switching versions asks you to resolve those changes first.
On a phone, opening an automation starts with compact navigation. The editor canvas fills the available height, and its run and deploy controls sit inside the canvas beside the zoom controls. Selecting a node opens its fields — and Save and Discard — in a panel at the bottom of the screen.

To switch without returning to the list, click the current automation's name in the breadcrumb trail. The menu includes every automation in the organization, even after switching to another project, except one bound only to projects you cannot open. Automations with no project assignments appear first, followed by those assigned to projects, with a horizontal divider between the two groups. Search by name or slug and select an entry. The switch keeps the current tab; from a run’s detail, it opens the other automation’s **Runs** list. A selected version number does not carry over: **Editor** opens the other automation’s latest saved version.
## Read the canvas
Each box is a node. Its label identifies the step and type; **Reads** lists the nodes whose outputs it uses. Arrows come from references such as `{{ nodes.draft.output.text }}`. Edit the reference to change the dependency. The canvas does not create dependencies by drawing an arrow.
Badges show conditions and loops: `when`, `else of`, `for each`, `repeat until`, and `continue on error`. A cycle warning means two or more nodes depend on one another; remove the circular reference before saving a runnable version.
## Edit a node
Select a box to open its fields. On a wide screen, the panel opens beside the canvas; until you select a node, the canvas takes the full width. On narrower screens, the fields open in a dialog over the canvas. A `transform` has **Code**; an `llm` has prompt, model, and output-schema fields; an `agent` also has harness and equipment. The **Model** picker of an `llm` or `agent` node lists the models your organization’s connected providers serve; a model that is not listed can still be typed, but validation warns that a live run would fail at that node until its provider is connected. **Input** contains JSON values and references passed to the node. Incomplete JSON is reported and does not update the node.
Open **Control flow** for conditions and iteration; it is already open on a node that has one. Use **Close** to return to the canvas. On a wide screen, clicking the empty canvas or pressing Escape outside a text field also closes the panel. The automation's trigger and project settings are on the **General** tab. [Automation concepts](https://docs.tale.dev/platform/automations/concepts) explains the node types and expression rules.
## Save and test a version
1. Edit the required fields and click **Save**.
2. Enter a **Version message** that explains the change, then **Save version**. This appends a version and preserves earlier ones. If someone saved another version while you were editing, Tale refuses the save and asks: **Discard my changes and reload** shows the newer version, **Save anyway** appends your version on top of it — the newer one stays in the version history, but the latest version is then yours.
3. Click **Test run**. If the workflow declares an input schema, fill **Run input (JSON)** in the dialog. Expand **Input schema** to inspect required fields and types. Invalid JSON or a schema mismatch prevents the start.
4. Start the test, switch to the **Runs** tab and inspect its row. Open it to compare the resolved input, output, and proposed operations with your expected result.
For a workflow requiring `owner` and `repo`, an input might be:
```json
{
"owner": "your-organization",
"repo": "your-repository"
}
```
Use the workflow's actual schema. A field typed as a number must receive a JSON number, not a quoted string. Where a project selector is offered, check the scope before starting.
**Test run** uses the selected saved version with deterministic mocks. It does not send mail or change external records. A draft can be tested before it is deployed; a successful mock does not verify live credentials or outside services.

## Deploy and run live
Choose the tested version under **Version** and click the deploy button beside it, which names that version, such as **Deploy v3**. The **Live** badge marks the deployed version. A version whose saved tests failed cannot be deployed; correct the cause and save a new version.
**Run live** starts the deployed version, even if you are viewing another one. Its confirmation shows the scope and, when required, **Run input (JSON)** for that deployed version. Check both before confirming. Live runs may act on connected systems and may wait for an [approval](https://docs.tale.dev/platform/approvals/concepts).
A trigger also runs the deployed version. Set it up only when you are ready for repeated or externally initiated execution; see [Automation triggers](https://docs.tale.dev/platform/automations/triggers).
## Diagnose a result
**Show last run** overlays run states on the canvas. Select a node to see **In this run**, its **Resolved input**, **Output**, and effects. This is often enough to find a wrong reference: compare the input received by the failed node with the output of the node it reads.
Switch to **Runs** and open a row for the full record. The tabs remain visible with **Runs** active; **Editor** returns to the workflow. Check whether it was a test or live run and inspect already completed operations before starting another run. [Execution logs](https://docs.tale.dev/platform/automations/execution-logs) explains waiting, failures, automatic retries, and stopping.
## Roll back or delete
To roll back, open **Version** at the right of the tab strip, read the version messages and select an earlier version. Its row opens **Editor** at that version; click its deploy button there, such as **Deploy v2**. Future starts use it; version history remains intact. A version message such as “Restore the previous recipient mapping” makes that choice easier to review.
To delete the automation, return to the list, open its row menu, and select **Delete**. Read the named confirmation. All versions, deployment, trigger, and project bindings are removed. An unfinished run blocks deletion; stop it or let it finish first. Past runs remain subject to retention. Deleting an automation does not undo the actions its runs already performed.
# Read automation runs and recover from failures
Source: https://docs.tale.dev/platform/automations/execution-logs
Open an automation, switch to its **Runs** tab and select a row to understand what happened. Start with its status, version and mode, then inspect the relevant node. A successful test run proves the workflow’s mocked execution; it does not prove that a real external account will accept the same action.

## Read the run’s state
The **Runs** tab lists the latest 50 runs you can see, newest first. Owners, Admins, and Developers see the organization’s own runs; a run in a project, and any question it waits on, appears only to those of them who can open that project. Each row identifies its version, time, mode and starter, or gives a failure or waiting reason. The detail shows the workflow with node results and run timing; an unfinished run has no completion time. The tabs stay visible while you inspect a run. Choose **Runs** to return to the list or **Editor** to change the workflow.
| Status | Meaning | What to do |
| --- | --- | --- |
| **Queued** | Accepted, waiting for execution. | Wait and inspect capacity if it does not progress. |
| **Running** | The engine is processing the workflow. | Follow node progress. |
| **Waiting** | A decision, reply, agent turn or polling condition is outstanding. | Read what it is waiting for. |
| **Succeeded** | Reached nodes completed and the workflow produced its output. | Review output and effects. |
| **Failed** | Execution ended with an unhandled failure. | Open the failed node and read its error. |
| **Stopped** | The run was cancelled. | Inspect work already performed before restarting. |
A waiting approval or question requires a person; a running agent or polling node may continue without you. A decision or answer can also be refused or expire. Use the displayed reason, not **Waiting** alone, to decide whether action is needed. [Approvals in workflows](https://docs.tale.dev/platform/automations/approvals-in-workflows) explains the decision controls.
## Inspect the node that matters
Select a node on the run’s canvas. **Resolved input** shows the actual values after template evaluation; **Output** shows what the step returned. These fields distinguish a bad reference from a service failure.
Node states include **Ran**, **Skipped**, **Failed**, **Never reached**, **Not reached yet** and, on a stopped run, **Stopped here** for the node the run was on when it was stopped. A skipped node may have a false condition, an unmet dependency, an alternate branch or a failure rule that permits continuation. Do not assume every skipped node is an error.
For example, a reminder node may receive a customer name but an empty invoice ID. Inspect its upstream output: if the record now uses another field, correct the reference there rather than replacing the mail credential. Verify the corrected resolved input in a new test run.
Applications reading the [run API](https://docs.tale.dev/develop/api-reference) also receive `failureCode` when a failed run has a classified cause. For example, `approval_rejected` means a person refused the operation, while `llm_output_invalid` means a model response did not match the required structure. Historical failures can lack a code. Use it to route an investigation; it does not establish that retrying is safe or will succeed.
## Check what the run changed
The effects list records connector writes, with the node, connector and input. A test uses deterministic stand-ins; live actions can change external systems. The run explicitly reports when it has no recorded effects.
Read effects before retrying. A failure later in the graph does not undo an earlier message or update. For delivery-sensitive work, confirm the result with the receiving service as well. Effects are retained with the run until deletion or retention removes that record; they are not a permanent independent archive. Deleting the automation keeps its runs: a run page still opens, marked with the deletion date and drawn from the run’s own trace, until retention removes it.
## Understand continuation and automatic retries
The engine checkpoints completed nodes and resumes after those checkpoints when execution continues. An unfinished run whose continuation was lost can be picked up after a grace period. A separate run starts with separate checkpoints and can repeat writes, so “run again” is different from resuming the existing run.
An eligible agent-step failure can receive up to three automatic retries after the original attempt. Upstream checkpoints remain intact, and the header reports **Auto-retry 1 of 3** and subsequent attempts. An attempt that performs at least fifteen minutes of execution refreshes that retry budget. Subscription pools can choose another account for a new attempt. When a subscription broker refreshed the account while the step worked and the provider rejected the old token, the retry continues on a fresh token without using one of the three retries; a third such interruption in a row counts like any other failure. When the step could not start because every account of the pool was cooling down after a rate limit, the retry starts once the first account is available again, at most a minute later, and continues the conversation the refused attempt was to resume. That wait uses one of the three retries unless the refused attempt was itself retrying a rate-limit failure. When the failed turn had announced its conversation handle, the retry resumes that conversation over the preserved workspace — the agent continues where the cut landed instead of reasoning from the start; a turn that died before announcing one, or whose sandbox session is gone, starts fresh. A Gemini CLI step always starts fresh, because that runtime cannot resume a conversation in which it ran a tool; [Choose an agent runtime](https://docs.tale.dev/platform/agents/harnesses) explains the exception.
An agent step also fails when its model returns nothing at all: no text, no tool call and no generated tokens. A model server can answer this way when it breaks down mid-answer. The run then reports “The model returned an empty answer, so the agent did nothing this turn.” This failure receives those retries too. A step whose model only used tools, or reported generated tokens but no visible text (reasoning, for example), has answered and does not fail this way.
An exhausted budget, full execution-window timeout, or expired question does not receive those retries. Each attempt consumes its own resources; retrying does not erase earlier charges. If repeated attempts cannot fix the cause, stop the run and correct the dependency before starting another.
## Stop or repair the workflow
Select **Stop the run** for an unfinished run you want to cancel, and confirm. Cancellation stops further work at the engine’s execution boundaries; it does not roll back completed effects. If the run finishes before the cancellation reaches it, its completed outcome is retained.
To repair a document problem, return to the editor, change the relevant input or node, and save a version with a useful message. Run a test with representative input and inspect the values and output, not only the success badge. Deploy that version when the result is ready. Scheduled and webhook starts then use the deployed version; an older failed run remains a record of the old version.
If the run never started, inspect its [trigger](https://docs.tale.dev/platform/automations/triggers). A disabled trigger, missing deployment or rejected input can explain the absence of a run altogether.
# Start automations automatically
Source: https://docs.tale.dev/platform/automations/triggers
Use the **Trigger** section on the automation’s **General** tab when work should start on a schedule or in response to an event. Every trigger starts the deployed version in live mode. Before enabling one, test the workflow with the input shape it will receive and check that its external actions are ready.

## Choose how the automation starts
| Trigger type | Use it for | Input passed to the run |
| --- | --- | --- |
| **Schedule** | Periodic work at a local time or regular interval. | `{ trigger: "schedule", firedAt: }` |
| **Webhook** | A delivery from another system. | `{ trigger: "webhook", payload: … }` |
| **Platform event** | A named event inside the organization. | `{ trigger: "event", event: "…", payload: … }` |
An automation has one configured trigger at a time. Changing its type replaces the previous binding. Replacing a webhook revokes its URL immediately; configuring another webhook later does not recover that credential.
An API or MCP client can also start work without a configured trigger. Its API key and project permissions authorize the request, and it sends the workflow’s input directly. See the [API reference](https://docs.tale.dev/develop/api-reference).
## Set a schedule
Open the automation, then its **General** tab. Without a binding, the **Trigger** section says that the automation runs only when started by hand or through the API; choose **Add trigger**, then **Schedule** under **Trigger type**. A new trigger starts with **Enabled** off — keep it off while preparing a workflow that should not start yet.
Fill **Cron** and choose **Timezone**. A cron expression has five fields: minute, hour, day of month, month and day of week. Use an IANA timezone such as `Europe/Zurich` when local business hours matter; an unspecified timezone means UTC.
Review the next occurrence shown for a valid expression: it is the minute the schedule will actually start, daylight-saving changes included. Then click **Save** beside the tabs. Confirm that the workflow’s deployed version accepts the schedule input above. When ready, turn on **Enabled** and save again. Check the next started run under **Runs**.
```text
*/15 * * * * every fifteen minutes
0 9 * * 1-5 09:00 on weekdays
0 6 1 * * 06:00 on the first of the month
30 8 1 * 1 08:30 on the 1st and on every Monday
```
Fields support `*`, numbers, ranges, steps and comma-separated lists. Both 0 and 7 mean Sunday. If both day-of-month and weekday are restricted, either match is enough; the last example runs on Mondays as well as the first day of each month.
Local time follows the timezone’s daylight-saving rules. A 09:00 Zurich schedule stays at 09:00 locally. Timing has one-minute resolution. Missed occurrences during an outage are not replayed; work resumes at the next occurrence. Impossible calendar dates are rejected when saving.
## Receive a webhook
Choose **Webhook**, then save to generate the credential. Copy the full URL when it appears: the token is shown once and only its hash is stored. The section supplies an organization URL and a project URL pattern. Use the project URL for an active project in which the automation is installed; an automation with project bindings cannot run through the organization-only URL.
Post a small payload to the URL. JSON becomes `payload` inside the input wrapper, not the workflow’s top-level input. Other request bodies pass through as text. The limit is 256 KiB; upload large documents separately. An accepted request returns a run ID without waiting for completion.
For example, a posted `{ "invoiceId": "inv-1" }` reaches the workflow as:
```json
{
"trigger": "webhook",
"payload": { "invoiceId": "inv-1" }
}
```
Use a delivery ID, such as `Idempotency-Key` or the sender’s supported delivery header. Repeating that ID within 24 hours returns the original run. Without an ID, an identical body on the same URL within two minutes is treated as a duplicate. Send distinct IDs if identical payloads represent separate work. [Webhooks](https://docs.tale.dev/develop/webhooks) lists supported headers, project routes, errors and response formats.
The URL authorizes a run. Store it as a credential and share it only with the sending system. **Rotate token** generates a replacement and invalidates the old URL; removing or replacing the trigger also revokes it. Update the sender after a rotation.
## React to a platform event
Choose **Platform event**, select **Event name**, then save and enable when ready. Match the workflow’s schema to the `trigger`, `event` and `payload` wrapper in the table. Events raised by automation runs do not fire triggers, preventing a workflow from repeatedly starting itself through its own changes.
A workflow expecting required top-level fields such as `owner` and `repo` cannot accept schedule metadata or a wrapped webhook unchanged. Adapt its input schema and references, or use an API-started run that supplies those fields. The trigger settings do not provide arbitrary saved input fields.
## Start a project agent on a schedule
A schedule can put one of a project's existing agents to work: a standing task the agent reports on at every occurrence, or recurring work you would otherwise start by hand. Install the automation in the task's project, add a `task.start_agent` step that names the task, and give the automation a schedule. Each occurrence starts the task's agent assignee, or first assigns the task to the agent `agentId` names, which must belong to the same project. `feedback` is the message the run addresses first, such as the occurrence it serves:
```yaml
nodes:
- id: start
type: task.start_agent
input:
taskId:
moveToInProgress: false
feedback: 'Scheduled occurrence {{ input.firedAt }}.'
```
The step answers the run it started, and the task's timeline lists that run as **automation** with a link to the automation run. When it starts nothing, the step still succeeds and says why, so the occurrence is recorded rather than queued:
| Answer | Meaning |
| --- | --- |
| `started: true` | The agent's run started; `runId` names it. |
| `already_running` | The task's previous run is still working and carries the work. Nothing new starts, and the occurrence does not wait behind it. |
| `in_review` | With `moveToInProgress: false`, the card waits for its captured reviewer, a person or an agent. Nothing is assigned or started, and the review keeps that recipient. |
| `closed` | With `moveToInProgress: false`, the card is **Done** or **Cancelled** (`taskStatus`). Nothing is assigned or started. |
| `agent_busy` | The agent is working another task (`busyTaskId`). An agent works one task at a time in its workspace. |
| `blocked` | A task this one depends on is still open (`blockedBy`). |
| `paused` | The task took three starts by automations and agents within the last hour, ordinary automatic retries included. One broker cooldown immediately after the same agent’s HTTP 429 adds no start; consecutive cooldowns still count. `retryAfter` says when the hourly count permits another start; other admission checks still apply. |
A run a schedule starts answers to no person. It works with the agent's configured instructions, secrets and tools, and its spend counts as automation spend against the organization's limits. Connector actions it asks the platform to run act for nobody, so they are refused. It keeps that authority only while the schedule may act in the project: turning the schedule off, removing it, or uninstalling the automation from the project stops the next start, fails a run that has not launched yet, and ends the workspace tools of a run in progress. A run a person starts from the automation answers to that person instead, as long as they can edit the project. A run a webhook or a platform event started cannot start agents, and an automation that is not installed in the task's project cannot reach its agents.
`moveToInProgress` decides what happens to the card. By default the card moves to **In progress** and the result waits at **In review** for its [configured reviewer](https://docs.tale.dev/platform/projects/tasks#review-default), as after **Start agent**; a review still pending on the earlier work is withdrawn, never approved. With `false` the card stays where it is and the run asks for no review, which suits a standing task in **To do**. The run keeps that choice to its end: when it completes, its report and files arrive as usual, and the card is neither moved nor sent for review, even if someone moved it to **In progress** meanwhile. That start only runs under open work (**Backlog**, **To do** or **In progress**): a card waiting at **In review** answers `in_review` and a closed one `closed`, so the card never presents earlier work for judgment, or as finished, while new work runs under it.
A retryable failure can be retried automatically while the task keeps its original status and assignee, with no later status, assignment, archive or review decision. Changing a value and then changing it back still ends that retry; comments do not. The attempt limits, schedule permissions and workspace checks still apply. A later scheduled occurrence can start fresh work when eligible.
### Import every issue on a schedule
An issue import reads at most one batch per run, up to 500 issues, and answers where the next batch starts. A person continues it with **Continue import**; a schedule keeps the position between its occurrences instead, so each occurrence imports one batch from where the last one stopped until every open issue has been read, and the occurrence after that starts the next pass. Read the position with `task.get_import_cursor`, pass it to the importer, and save the importer's `nextCursor` with `task.save_import_cursor`:
```yaml
nodes:
- id: position
type: task.get_import_cursor
onError: continue
input: { projectId: , externalSystem: github, source: owner/repo }
- id: issues
type: subautomation
automation: github-import-issues
onError: continue
input:
projectId:
owner: owner
repo: repo
limit: 500
cursor: '{{ nodes.position.output.cursor }}'
- id: progress
type: task.save_import_cursor
onError: continue
input:
projectId:
externalSystem: github
source: owner/repo
revision: '{{ nodes.position.output.revision }}'
next: '{{ nodes.issues.output.nextCursor ?? "" }}'
```
`source` is your name for the listing; automations that name the same source share one pass. The read also answers `revision`, the position's compare token, and the save hands it back: the position advances only while it is still at that revision. Every saved batch, every drain and every restart moves the revision on, and a revision never repeats, even when the cursor text does. An import that fails saves nothing, so the next occurrence retries the same batch. A save from any earlier revision is refused (`conflict`) and writes nothing, whether it comes from an overlapping run, from a run delayed past the end of its pass, or from a run that still holds a position from before a restart. After three reads of one position without a save, the next read starts the pass over (`restarted`) instead of retrying a position the source keeps refusing, for example after the repository was renamed. The save answers `batch` and `drained`, which a receipt can report. Each run also refreshes up to 500 issues imported earlier, the longest-unchecked first, so a large collection is refreshed over several occurrences.
## Diagnose a missing start
First check **Enabled**, the deployed version and the last-fired information. Then inspect any recorded skip reason:
| Reason or symptom | What to check |
| --- | --- |
| `not_deployed` | Deploy a tested version. A saved draft is insufficient. |
| `start_refused` | Compare the deployed input schema with the trigger’s actual wrapper and resolve the reported validation or start error. |
| `unusable_cron` | Correct the expression or timezone and save it again. Other schedules continue while this one is skipped. |
| `paused_after_failures` | The schedule turned itself off after repeated failures. See [When a schedule pauses itself](https://docs.tale.dev/platform/automations/triggers#when-a-schedule-pauses-itself). |
| Webhook credential refused | Check the current URL and enabled state. Unknown and disabled tokens intentionally receive the same refusal. |
| Run exists but did not finish | Open [execution logs](https://docs.tale.dev/platform/automations/execution-logs); the start succeeded and the issue is inside the run. |
The last-fired timestamp advances when a run actually starts. A due trigger that cannot start work records a skip instead. This separates a broken schedule from a workflow that started and later failed.
## When a schedule pauses itself
A schedule whose runs fail the same way at every occurrence would otherwise keep failing indefinitely. Tale counts the runs a trigger starts that fail with an error a retry won't fix: the automation's own code (`node_error`), a connector (`connector_error`), a model reply that does not match its schema (`llm_output_invalid`), or the organization's model provider (`auth_error`, `missing_api_key`, `credit_exhausted`, `model_not_found`). A successful run resets the count, and other failures, such as a rate limit or an unreachable provider, neither count nor reset it. A schedule that has already paused itself is the exception: it keeps the count that paused it, even when a run that was still in progress succeeds afterwards, and only saving the trigger clears it.
After five such failures in a row, the schedule turns **Enabled** off and records `paused_after_failures`. The **Trigger** section then shows the pause, the last failure's code and time, and **View run**, which opens that run. While runs are failing but the schedule is still on, the section shows how many failed in a row. Owners and Admins get a bell notification, which also reaches them by email when the organization has a connected mailbox, and the audit log records the pause. They can switch these notices off with **Automation alerts** under **Settings > Notifications**.
Open the failed run to read the error, and fix the automation or its connection. Then turn on **Enabled** and save. Saving the trigger starts a new count, whether it turns the schedule back on or leaves it off, and marks the notices read.
Webhook and platform-event triggers count failures the same way but are never paused. Their runs carry a delivery or an event, which a paused trigger would drop.
## Pause or replace the trigger
Turn off **Enabled** and save to pause starts while retaining the configuration and run history. Re-enable it to resume. **Remove trigger** deletes the binding; for a webhook, its URL becomes unusable.
Triggers belong to the automation’s name, not a version. Deploying or rolling back keeps the same schedule or URL and changes which version future starts execute. Editing a trigger does not create a workflow version, so review trigger settings alongside any deployment that changes expected inputs.
# Compare models in Arena
Source: https://docs.tale.dev/platform/chat/arena-mode
Use **Arena Mode** to compare two models on the same prompt. Both sides use the chat assistant and the same starting context. Choose a question you can evaluate: preference alone cannot establish that an answer is correct.
## Start a comparison
Open a private chat, open the composer’s **+** menu and select **Arena Mode**. Shared chats cannot enter Arena. Choose the models under **Model A** and **Model B**, then send your prompt. You may choose the same model twice to compare variation, or two different models to compare their behavior.
For a useful first comparison, give both sides a short source and a precise request, such as “List the three decisions in these meeting notes and cite the sentence supporting each.” Keep the source, instructions and requested format the same.

Each reply appears in its own column. The prompt is admitted for both columns together: a usage limit that would stop it stops it for both sides, never for one column alone. Wait until both have finished before selecting a verdict; the controls stay unavailable while either side is answering. A slow reply is still part of the comparison. If a side fails, its column shows the error and the round cannot be judged: the four verdict buttons stay unavailable until both columns hold a finished reply, while **Exit without verdict** remains available. Inspect the error before treating the result as a quality judgment.
## Judge the replies
Check factual accuracy against the source, whether the reply followed the instructions, whether essential details are missing, and how much editing you would need before using it. A longer or more confident response is not necessarily better.
| Verdict | Use it when | Conversation continues with |
| --- | --- | --- |
| **A is better** | A is more useful or accurate. | Column A. |
| **B is better** | B is more useful or accurate. | Column B, and the composer switches to Model B. |
| **Tie** | Both meet the request equally well. | Column A. |
| **Both bad** | Neither is acceptable. | Column A. |
| **Exit without verdict** | You do not want to record a comparison. | Column A, without a verdict. |
Every choice ends the two-column comparison. The next message goes to the remaining conversation. To compare again, enable Arena again; a tie does not keep both columns active.
The other column's answer is discarded and goes to the organization's [Trash](https://docs.tale.dev/platform/admin/governance/trash), where an administrator can restore it as a chat of its own until the retention grace window ends. It no longer appears in your chat list or in search, and a link to it reports that the chat is not available.
## Find the recorded feedback
When both models have produced replies, a verdict contributes to the organization’s [Feedback analytics](https://docs.tale.dev/platform/admin/governance/feedback-analytics). Administrators can inspect **Arena verdicts** and model matchups there. A round in which only one column answered is never recorded: the verdict is refused, so the analytics only contain comparisons of two finished replies. Exiting without a verdict does not add a rating.
Use several representative questions before drawing a conclusion about a model. A result for a short summary may not predict its performance on code or long documents, and organization-wide preferences include other people’s tasks.
## Resolve a blocked comparison
If a model is absent, check its provider and access rules through the [model catalog](https://docs.tale.dev/platform/models). If the verdict buttons remain disabled, wait for both generations to end, or check that both columns show a finished reply: a side that failed or never answered leaves nothing to compare, so exit without a verdict, fix the cause and send the prompt again. A failed request may reflect credentials, availability or policy rather than answer quality; use its displayed reason to decide what to fix before retrying.
# Ask about files and images
Source: https://docs.tale.dev/platform/chat/attachments
Attach a file when it is needed for the current conversation. The assistant receives images, retrieves text from supported documents, and reads transcripts of recordings. For files that several chats should reuse, upload them to a [project](https://docs.tale.dev/platform/projects/manage-files) or the [Knowledge library](https://docs.tale.dev/platform/knowledge/documents) instead.
## Add an attachment
Open the `+` menu beside the message field and choose **Add photos & files**. You can also drag files onto the composer or paste a screenshot into the message field. A message can carry up to ten files.
Images appear as thumbnails; other files appear as named chips with their processing status. Check the filenames before sending. Use a staged attachment’s remove control to leave it out of the message.
After sending, **Edit message** changes the text in a new version of the conversation and keeps the original attachments. You do not need to upload them again.

Write a question that tells the assistant what to look for, such as “Read the meeting note and list the decisions, their owners, and any missing deadlines.” Attaching a file without a question leaves the intended task unclear.
When you try to attach audio or video, Tale checks whether the organization has an available transcription model. If the check prevents the upload, a dialog explains the problem and offers the recovery actions you can use. Those files are refused before transfer; other supported files in the same selection can still upload. Close the dialog to keep composing, follow the settings action if you have access, or ask an admin to check [Models](https://docs.tale.dev/platform/admin/governance/content-models).
## Understand what the assistant receives
| Attachment | What the model uses | What to check |
| --- | --- | --- |
| Image or pasted screenshot | The image itself, when the selected model supports vision. | Confirm the picker offers an image-capable model and the text is legible. |
| PDF, modern Office document, or supported text file | Text made available through the assistant’s retrieval tools. | Wait for processing and check for a reading step in the reply. |
| Audio or video file | A text transcript. | An admin must configure transcription; check names, numbers, and specialist terms against the recording. |
| Legacy Office file without a text extractor | The filename, without searchable document text. | Save it as `.docx`, `.xlsx`, or `.pptx` and attach that copy. |
File upload support and text extraction are separate. A file that appears in the chat is not necessarily a file whose contents the assistant can read.
## Send while processing continues
If documents or recordings are still processing when you send, Tale queues the message and sends it after they are ready. The queued message appears above the composer. Cancel it there if you need to change the question; its text returns to the field.
Paste a supported video link into the message field to start creating an attachment. A URL entered by typing stays ordinary message text. Tale retrieves captions first and uses audio transcription when captions are unavailable, then supplies the transcript to the assistant. When the captions show two people speaking at the same moment, the transcript keeps the words of both. Pasting a link remains available without a transcription model because usable captions do not need one. Pasting a link again in an existing chat before sending reuses the attachment that chat already has for it, even if you pasted the link in another chat in between; each chat keeps its own. A failed or removed attachment, or one added more than a day earlier, is not reused. If that link fails, retry it or remove it before sending. If Tale cannot remove a link, its chip comes back and a message says why; the link stays attached until a removal succeeds.
A model change applies to new transcription work; completed attachments keep their existing transcript. Uploading the same bytes again reuses completed work for the same transcription target, but transcribes them again when the target provider or model differs.
## Keep the file in the right place
Chat attachments belong to that conversation. They do not automatically enter the organization’s Knowledge library or become available in another chat. Switching conversations clears staged attachments, so check the chips again after changing chats.
A regenerated reply uses the original message’s stored attachments. If you need to ask about a different version of a file, send the new file and identify which version the assistant should use.
For recurring questions about a brief or policy, put the file in the appropriate project once. Start later chats inside that project instead of uploading a separate copy each time.
## Resolve attachment problems
| Symptom | Action |
| --- | --- |
| The image cannot be read by the selected model | Choose a vision-capable model. On Auto, Tale considers compatible image models; if none are available, ask an admin to configure one. |
| Processing fails | Retry the file. If a small supported file also fails, ask an admin to check storage, indexing, or transcription as indicated by the error. |
| The assistant knows the name but not the contents | Check the format and processing status. Convert legacy files to a supported modern format. |
| The answer invents detail from a recording | Check the transcript against the recording and supply the corrected passage before continuing. |
| A queued message has not sent | Inspect every attachment’s status, including video links. Remove or retry failed items. |
| A removed video link comes back | Tale could not remove it, and the message that appears says why. The link is still attached. Remove it again once the cause is resolved, for example when your connection is back or after you sign in again. |
Return to [Chat basics](https://docs.tale.dev/platform/chat/basics) to check sources and continue the conversation.
# Ask questions in chat
Source: https://docs.tale.dev/platform/chat/basics
Use chat to ask questions, understand a document, or investigate information in Tale. The assistant can search accessible knowledge and read public pages. Start with a specific question, then use follow-up messages to narrow the answer.

## Send your first message
Open **Home**. On a computer, it reopens the chat you last read, if there is one. To begin a new subject, choose **New chat** at the top of the Home list or, on a computer, choose **Home** again while it is active. Type in the message field. Press **Enter** to send or **Shift+Enter** for a new line. A starter prompt fills the same role as your own first question; edit your request to include the source, subject, and kind of answer you need.
For example: “Find the onboarding feedback and summarize the three most common problems. Cite the documents and separate reported problems from your suggestions.”
If your organization turned on a confidentiality notice, it appears under the message field as a reminder of what not to share in chat.
While the reply streams, the send control becomes a stop control. Stopping keeps the text already received, which may end mid-sentence. Use a follow-up message to clarify the question or ask for missing detail.
## Choose a model when the choice matters
The model picker starts on **Auto** when several usable models are available. Auto selects a model for each message from your organization’s available models; organization rules can set a default or restrict the choices. The details under a reply identify the model that actually answered.
Pick a named model when you need consistent comparisons or know which model the work requires. Your choice stays selected until you change it, including which provider serves the model when two providers offer the same one. If that model supports adjustable reasoning, the picker also offers an effort setting. More reasoning can take longer; it is not a substitute for checking the answer.

If no models are available, ask an admin to check active provider credentials and model access. Chat offers only models that an API key or environment-variable credential serves: a subscription runs only in tasks and automations, and the model list names any it leaves out. [Models](https://docs.tale.dev/platform/models) explains how the catalog is built.
## Give the assistant the right sources
Choose the conversation’s location before asking about files:
| Where you ask | Files the assistant can retrieve |
| --- | --- |
| The organization’s chat | Knowledge-library documents you can access and this chat’s own attachments. |
| A chat inside a project | That project’s files, Knowledge-library documents you can access, and the chat’s own attachments. |
| A shared chat link | A read-only snapshot; viewers cannot ask follow-up questions there. |
Project chat also receives the project’s standing instructions. File access is enforced by Tale, so asking a project chat to read a different project does not grant access. Trashed or expired files are not searchable.
Use [attachments](https://docs.tale.dev/platform/chat/attachments) for material needed in this conversation, [project files](https://docs.tale.dev/platform/projects/manage-files) for recurring project work, and [Knowledge](https://docs.tale.dev/platform/knowledge/overview) for shared reference material. The assistant retrieves content when it needs it; uploading a document does not mean every answer has read it.
Questions about Tale itself need no upload: the assistant looks up the public documentation at docs.tale.dev before explaining how a screen or setting works. If the server cannot reach docs.tale.dev, for example on a self-hosted installation without internet access, the timeline shows a failed reading step and the answer is not grounded in the documentation. The documentation describes the latest release, so the assistant points out when your workspace may differ.
## Check what the assistant used
Above the reply, the timeline shows search and reading steps. A failed step explains what could not be read; it is useful evidence when an answer is incomplete. Expand the thinking section when one is available, but judge factual claims against sources rather than the fluency of that explanation.
**Sources** below the answer lists documents and pages the assistant loaded. Open a source and check that it supports the relevant claim. A citation establishes which material was used, not that every conclusion is correct. A reply without a retrieval step may rely on the model’s prior knowledge.
The assistant can search workspace information such as documents, knowledge entries, websites, contacts, products, accessible tasks, and the Inbox conversations you can see, including the text of the emails they received and of their attachments. A task can be named by its key, such as `DOCS-12`, as the board shows it. It can fetch the details behind a result and read a public web page. Chat does not run code, change connected systems, create images, produce file deliverables, or use [skills](https://docs.tale.dev/platform/workspace/skills); assign that work to a [project task](https://docs.tale.dev/platform/projects/tasks). Any member can create one in a project they can open and hand it to one of the project's agents; [Turn a chat into a task](https://docs.tale.dev/platform/chat/basics#create-task-from-chat) shows how to start it from the conversation. A project agent working on the task can create images when an admin has turned on [image generation](https://docs.tale.dev/platform/admin/governance/content-models#let-agents-generate-images).
## Turn a chat into a task {#create-task-from-chat}
When a conversation ends in work that needs a file, such as a presentation, a report, or a spreadsheet, hand it to a project agent. Select **Create task** in the conversation's header; on a narrow screen, choose **Create task from chat** in the **⋯** menu. A chat filed in a project creates the task there. Otherwise, choose the project first: **With an agent** lists the projects you can open that have agents, with how many. A project without agents of its own appears there with **Standard agent**: its task goes to the organization's [standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent). When the standard agent can't run for you, for example because an admin has turned it off, such projects are listed under **No agent yet** instead, each saying who can add one.
The task dialog opens with your last request as the description, a link back to the chat, and the files you attached in the conversation. When the project has a single agent, or uses the standard agent, it's already the **Assignee**; otherwise, choose one. Edit anything you like, then select **Create and start agent**: the task is created, and the agent starts on it at once. **Create only** creates it without starting the agent; **Start agent** on the task starts it later.

The task then shows above the chat's message box with what it's doing: **The agent is working**, **Waiting for a sandbox slot**, **Trying again…**, **Ready for review** with the number of files it delivered, or **The agent couldn't finish**. **Open** takes you to the task. You're also notified when it's ready for review and when the agent can't finish; [When the agent can't finish](https://docs.tale.dev/platform/projects/task-automation#when-the-agent-cant-finish) explains what to do next.

Ask the assistant for such a file, and it answers what fits in a reply, then walks you through these steps for your own projects, naming the buttons as you see them.
No project you can open offers an agent? Then the standard agent can't run for you: an admin may have turned it off under [Governance > Models](https://docs.tale.dev/platform/admin/governance/content-models#standard-agent), or no model you may use can run it. Ask an Admin about it, or ask an Editor or Admin to add an agent on the project's **Agents** tab. As an Editor, you can also add one from the task: under **Assignee**, choose **Create an agent…**.
Files come along only from your own conversation. A task takes only its creator's uploads, so a task created from a chat someone shared into a project starts without them.
## Continue, copy, or keep the conversation
Use the reply toolbar to copy an answer, give feedback, inspect its details, or fork a conversation at that point. A fork lets you try another direction while preserving the earlier exchange.
Find earlier chats in [Home](https://docs.tale.dev/platform#home); **Chats** above the list hides your tasks and inbox conversations. Pin frequently used chats, give a chat a recognizable title, or move it into a project when the topic becomes ongoing work: drag it onto the project or choose **Move to project…** in its menu. [Shared chats](https://docs.tale.dev/platform/chat/shared-threads) explains how to publish a read-only snapshot for colleagues.
Very long conversations may exceed the model’s context window. Tale displays a notice when older messages are omitted. Restate an important requirement or start a new chat with the relevant sources instead of assuming the assistant still sees the entire history.
## Improve an incomplete answer
| Problem | Try this |
| --- | --- |
| The answer is too broad | Ask one question, name the audience, and specify the desired length or format. |
| A file was not used | Check the chat’s project, the file’s indexing status, and the retrieval steps. Name the file explicitly. |
| Search reports an unavailable source | Ask an admin to check the named service or embedding configuration; an empty result is not proof the information does not exist. |
| A reply stops with an error | Read its error, check the selected model, and retry after the cause is resolved. Tale does not silently switch providers. |
| A reply ends without an answer | The note in its place says why: the model returned nothing, used up its output token limit before writing, or the provider’s content filter withheld the reply. Select **Try again**, or first lower the reasoning effort, shorten the request, or choose another model. |
For a guided example with source checking, follow [Chat effectively](https://docs.tale.dev/tutorials/member/chat-effectively).
# Chat
Source: https://docs.tale.dev/platform/chat/overview
Chat is the quickest place to ask about a document, understand a topic, or refine an answer through follow-up questions. The assistant can search accessible knowledge and fetch public pages when the question needs them. Check its sources before relying on a factual answer, especially when the information may have changed.

## Start with a useful question
Open **Home**, choose **New chat**, type in the composer, and send. A starter prompt can help if you are unsure where to begin. Include the outcome you need and the relevant source: “Using the onboarding guide, list the steps a new customer needs to complete. Cite the guide.”
When several usable models are available, the model picker can start on **Auto**. Select a named model when you want control over which available model answers, and adjust reasoning effort where supported. You can attach a file for a question about its contents or open a project chat for recurring reference material.

## Choose the surface for the work
| You want to | Start in |
| --- | --- |
| Ask, compare information, or clarify an answer | A **chat** |
| Reuse the same files and instructions across conversations | A **project chat** |
| Produce a deliverable with an owner and a review | A project **task** |
| Run a repeatable process on a schedule or incoming event | An **automation** |
Chat focuses on conversation and retrieval. For a substantial deliverable, the assistant may direct you to a task so an agent can work in a sandbox and a person can review the result. See [Tasks](https://docs.tale.dev/platform/projects/tasks) and [Automations](https://docs.tale.dev/platform/automations/concepts).
## Use the conversation well
Send, choose a model, read sources, and resume a conversation later.
Add a document or image and understand upload and indexing states.
Compare two answers to the same question and keep the one that works better.
Dictate a message or listen to a response, with the required provider setup.
Publish a read-only snapshot for your organization and stop sharing later.
Work through a focused question, a source check, and a useful follow-up.
# Share a chat with your organization
Source: https://docs.tale.dev/platform/chat/shared-threads
Share a chat when a colleague needs to read the question and answer without continuing your conversation. The link publishes a snapshot for signed-in members of your organization. It is not a public link, and it does not update automatically as you keep chatting.
## Create the snapshot
1. Open the chat’s header menu and choose **Share**.
2. Select **Share with organization** in the **Share chat** dialog.
3. Click **Create share link**.
4. Use **Preview** to check the snapshot, then **Copy link** when it is ready to share.
The snapshot is the branch you are viewing. If you used **Edit message** or **Try again**, the link publishes the version of each edited or regenerated turn that is on screen. Switch to the version you want colleagues to read with **Previous branch** and **Next branch** before you create the link.
Read the transcript before distributing the link. A reply can contain details copied or summarized from a restricted source; sharing that reply makes its text readable to the organization members who have the link.

## Include a later answer
New messages stay outside the published snapshot. Open **Share** again and choose **Include newer messages** when you want the same link to show the newer exchange. The snapshot is taken again from the branch you are viewing, so switch to the version you want to publish first. Preview it again before treating that version as the one colleagues should read.
A recipient who wants to continue the topic starts their own chat. A snapshot does not create a collaborative conversation, and it does not grant access to every source mentioned in it.
## Stop sharing
Choose **Keep private** in the share dialog or **Stop sharing** in the chat's menu in the Home list. The link becomes unavailable. Deleting the original chat also makes its share unavailable.
If the dialog can't load the sharing status, it selects neither option and says that the chat may still be shared. Choose **Try again**; once the status appears, you can choose **Keep private**. An existing link keeps working until then.
Stopping a share prevents future viewing through that link. It cannot retract text someone already copied, which is why reviewing the snapshot before sharing matters.
## Share with a project instead
For ongoing collaboration, use **Share with project** on a chat in a [project](https://docs.tale.dev/platform/projects/concepts). The project’s **Chats** tab separates personal chats from those shared with the project. Project membership alone does not share your personal chats automatically.
Arena comparisons cannot be shared while they remain Arena chats. Finish the comparison before sharing the conversation you keep; see [Arena Mode](https://docs.tale.dev/platform/chat/arena-mode).
# Voice mode
Source: https://docs.tale.dev/platform/chat/voice-mode
Dictation lets you speak a message instead of typing it. Voice output reads an assistant reply aloud. You can use either on its own: dictating does not require spoken replies, and listening does not require microphone access.
## Dictate and check a message
1. Click **Start dictation** on the composer's microphone control.
2. Allow microphone access in the browser if asked, then speak clearly.
3. Click **Stop dictation**. Where server transcription is used, wait for it to finish.
4. Read and correct the text in the message field, especially names, numbers, and dates. Send when it is ready.
Dictation adds text to the composer; it does not automatically send the message. Sending stops an active dictation. The chat model receives the text you submit.
Tale first uses the browser's speech-recognition capability when available. Otherwise, it can record and transcribe through your organization's configured transcription model. Browser speech recognition may use a browser-vendor service; do not assume dictation works offline.
The server fallback needs browser recording support through MediaRecorder, microphone permission, and an available organization transcription model. An admin chooses **Audio transcription model** under [Settings > Governance > Models](https://docs.tale.dev/platform/admin/governance/content-models). This setting also serves audio and video attachments. It does not change the browser's own recognition service: supported browser dictation remains usable when the organization's server model is unavailable.
If you try to start server dictation and its transcription model is unavailable, a dialog explains the problem. Follow the recovery action offered: open settings, retry a temporarily failed availability check, or ask an admin for help. You can close the dialog and continue typing.
## Listen to a reply
Enable **Voice mode** in the composer to hear replies in the current chat. A text-to-speech model prepares audio from the answer. Use the reply's playback control to stop or play it again; the written response remains available for checking details.
An organization policy can hide voice output. A disabled control can also mean there is no usable speech model. An administrator checks the configured [AI providers](https://docs.tale.dev/platform/admin/providers); changing the chat model alone does not supply a speech provider.
The voice setting on an existing chat applies to that chat. Setting it on a new chat also establishes the default for later chats. There is no separate voice configuration on each project agent.
## Recover a voice problem
| Symptom | What to check |
| --- | --- |
| The microphone will not start | Browser microphone permission, the selected input device, and whether another app is using it. |
| A message says dictation is unavailable | The browser's speech service could not be reached: check the connection, a proxy or a content blocker, then try again. |
| Words are missing or wrong | Reduce background noise and correct the transcript before sending. |
| Server transcription failed | Use the retry control while the failed recording remains available, or discard it and type. |
| A reply is ready but silent | Check device volume and browser playback permission, then use the reply's play control. |
| Voice reports a configuration error | Ask an administrator to check the speech model and credential. |
A failed server-transcription recording is held in the current page's memory for retry. Leaving or reloading the page can lose that recording. It is not a saved audio attachment; use [attachments](https://docs.tale.dev/platform/chat/attachments) when you want to upload an existing recording.
## Understand the audio path
Browser dictation follows the browser's speech service. The server fallback sends the recording to Tale for transcription with the organization's configured provider; this dictation path does not store it as a document. Once sent, the transcript becomes part of chat history.
Voice output sends answer text to the configured speech provider and streams audio for playback. If the answer contains information from a restricted source, that text is included in the speech request. Administrators should choose speech services consistent with the organization's [data-residency requirements](https://docs.tale.dev/cloud/data-residency).
# Connect an external client with MCP
Source: https://docs.tale.dev/platform/connectors/mcp-servers
Tale exposes an MCP endpoint that lets an external coding assistant or other MCP client work with your organization. Use it to discover capabilities, author automations, and inspect runs through the client’s tools. Access follows the organization API key and its holder’s permissions.
## Find your endpoint
Open **Settings > API > MCP**. The page shows the deployment’s endpoint URL, the organization slug, available tool groups, and a request you can copy to check connectivity. Create a suitable key under **Settings > API** if you do not already have one.

Follow [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint) for client configuration, authentication, and permission requirements. Store the key in the client’s credential settings; do not put it in a prompt or a shared document.
## Choose the direction of the connection
Tale’s MCP endpoint accepts connections from external clients. Tale does not provide a settings form for registering an external MCP server as equipment for its own project agents.
For an agent inside Tale that needs another service, check the [connector catalog](https://docs.tale.dev/platform/connectors/overview). When there is no suitable connector, a [project agent](https://docs.tale.dev/platform/projects/project-agents) can use an appropriately scoped secret to call a service from its sandbox. That grants the running agent access to the secret, so choose the scope for the specific job.
## Verify access before authoring
Start with the connectivity request on the MCP page and confirm that the client can list the tools. Then read the tool’s required permission before trying a write. Saving an automation and deploying it are separate operations; connecting a client does not bypass the deploy gate or approval rules.
Use [API keys](https://docs.tale.dev/platform/admin/api-keys) for key rotation and revocation, and [Automation concepts](https://docs.tale.dev/platform/automations/concepts) for the save, test, and deploy lifecycle.
# Connect Tale to external services
Source: https://docs.tale.dev/platform/connectors/overview
Use a connector when Tale needs to read or change data in an external service. The connector defines supported actions; a credential authorizes the account those actions use. A Developer, Admin or Owner manages credentials under **Settings > Connectors**.
## Choose the connection for the task
| Connector | Typical use | Authentication |
| --- | --- | --- |
| Confluence | Import Confluence Cloud pages into knowledge. | Username and password/token pair. |
| Discord | Work with messages and channels. | Token. |
| GitHub | Read or manage repositories, issues and pull requests. | Token. |
| GlitchTip | Read a project's issues so the GlitchTip importer can turn them into tasks. | Token and an Instance URL. |
| Gmail | Read, send and organize mail. | OAuth. |
| Google Drive | Import files into knowledge. | OAuth. |
| IMAP / SMTP Mailbox | Read or send mail through a private mail service. | Username and password. |
| Jev decisions | Answer typed questions about a workflow's data with calibrated probabilities, to decide whether to act. | API key (an OpenRouter key). |
| Microsoft Outlook | Work with mail, calendars and contacts. | OAuth. |
| Shopify | Work with products, customers and orders. | API key. |
| Slack | Work with messages and channels. | OAuth. |
| Tavily | Search the web and extract pages. | API key. |
| Microsoft Teams | Work with messages and channels. | OAuth. |
| Twilio | Send SMS and make voice calls. | Username and password/token pair. |
| WebDAV Files | Read, write and list the organization’s WebDAV files. | Username and password. |
The deployed catalog’s cards show the current actions and authentication methods. These definitions arrive with the platform; adding an account does not install arbitrary new connector code.
Knowledge imports use the [document indexing pipeline](https://docs.tale.dev/platform/knowledge/documents). OneDrive and SharePoint use the import flow in **Knowledge > Documents**, with per-user consent, rather than a separate organization connector. Mounting Tale’s documents on your own device is the other direction; use [WebDAV](https://docs.tale.dev/platform/connectors/webdav).
## Add the intended account
Select **Add credential**, search for the service and choose its card. Connectors with existing credentials appear first, but you can add another account for the same service. The form asks for the authentication that connector supports.

The **Name** field starts with the connector’s name. When you add several accounts for the same service, change it to one that identifies the purpose, such as `Support inbox` or `Release bot`. Use the external service’s credentials, not a Tale API key. For OAuth, sign in to the provider as the account you want to add and complete its consent. Each connection adds a new credential named after the connector and numbered (`Gmail`, then `Gmail 2`); Slack keeps one per workspace. Rename a new credential so the accounts stay distinguishable. If consent cannot start, an administrator may need to configure its OAuth app first.
Confluence, GlitchTip and Shopify require an **Instance URL** per credential. Use the Atlassian site origin, the GlitchTip instance origin such as `https://app.glitchtip.com`, or the store’s `myshopify.com` origin, rather than an unrelated page or customer-facing domain. A self-hosted GlitchTip instance must also be allowed by the deployment's connector host policy. [Connector credentials](https://docs.tale.dev/platform/admin/connectors) covers setup fields, reconnection and rotation.
## Choose which account an action uses
An action uses the credential it explicitly names, or the connector’s default when no name is supplied. Only one credential per connector is the default. With no default, an unnamed call fails even if other credentials exist. Deleting the default makes the oldest remaining active credential of that connector the default; the delete confirmation names it first.
For example, two support mailboxes are two credential rows. Choose names that distinguish them and inspect a workflow’s resolved input before running it live. A default is a fallback for selection, not proof that every job should use that account. Mailbox operations designed to inspect all active accounts are a separate case.
Disabling a credential retains its configuration but stops use through it. Replacing its secret updates the account connection used by existing references. Check dependent workflows before disabling, deleting or changing the default.
## Understand reads and writes
Automations use connector actions as workflow nodes. Each action declares an input schema, output and read or write effect. GlitchTip and Jev decisions only read: the GlitchTip importer writes Tale tasks and never changes the upstream issue, and a Jev decision answers each question with its probabilities and writes nothing. Each decision is billed to the OpenRouter account whose key the credential holds. In a test run, connector responses are mocked. In a live run, a write can send a message or change external data and is subject to the organization’s approval policy.
Equipped project agents receive supported read actions through Tale’s connector broker. It keeps those connector credentials outside the sandbox and returns results. The broker refuses connector writes. Direct GitHub tooling or explicit agent secrets use separate paths and must be reviewed separately.
Adding a credential does not add arbitrary tools to the ordinary Chat assistant. Use [automations](https://docs.tale.dev/platform/automations/editor) for a defined connector workflow and [project agents](https://docs.tale.dev/platform/projects/project-agents) for sandbox work.
## When the service is missing
An equipped agent can use a narrowly scoped secret to call a service from its sandbox. A `transform` node only reshapes data and cannot make an external API request. Review the permissions and expected effects before choosing a direct integration.
If the external application needs to call Tale, use the [REST API](https://docs.tale.dev/develop/api-reference) or [MCP endpoint](https://docs.tale.dev/develop/mcp-endpoint). [MCP and custom integrations](https://docs.tale.dev/platform/connectors/mcp-servers) explains that distinction.
# Open Tale documents through WebDAV
Source: https://docs.tale.dev/platform/connectors/webdav
WebDAV lets a compatible file client read and edit Tale’s organization documents as a remote folder. Changes use the same document store as **Knowledge > Documents**. Project-specific Knowledge files are not included in this mount.
## Get the connection details
Open **Settings > API > WebDAV**. Owners, Admins and Developers can generate their own device credentials. Copy the displayed URL, including the organization slug and `/documents/` path; do not construct it from an organization ID or use another organization’s address.

Use your Tale account email as the username and an app-password as the password. Your normal account password does not authenticate WebDAV. On a deployed service, connect over HTTPS; avoid credentials embedded in URLs or saved in command history.
## Generate one password per device
1. Select **Generate** and enter a **Label** such as `Design laptop`.
2. Generate the password and copy it before closing the result. The full value appears only once.
3. Store it in the device client’s credential manager, then select **I have saved it**.
The list keeps the label, prefix and usage dates, not the recoverable password. If you lose it, generate a replacement and revoke the old one once you have updated the client. Separate passwords let you disconnect one device without changing every other connection.
## Configure your client
In Finder, press **⌘K** to open **Connect to Server**. Paste Tale’s WebDAV URL and connect with your email and app-password. Open the mounted folder and inspect a known document before copying files into it. Save the credential only on a device you trust.
Use File Explorer’s network-drive connection with the HTTPS WebDAV address and your generated credentials. The Windows WebClient service must be available. If connection or large transfers fail, check Microsoft’s [WebDAV client requirements and limits](https://learn.microsoft.com/en-us/iis/publish/using-webdav/using-the-webdav-redirector) with IT, or use a dedicated WebDAV client. Keep HTTPS authentication enabled.
A desktop file manager with WebDAV support can use the displayed host and path. GNOME Files uses `davs://` for secure WebDAV; KDE Dolphin uses `webdavs://`. If the dialog separates server and folder, enter the host in the server field and `/dav//documents/` as the folder, with HTTPS and the correct port.
Choose a client that explicitly supports WebDAV and give the device its own app-password. Tale’s browser-based Documents page also works for occasional access. Do not assume the Files app’s generic server dialog supports this protocol. Direct WebDAV upload from Pages, Numbers and Keynote is [no longer supported](https://support.apple.com/en-us/101948).
Run `rclone config` and create a WebDAV remote with Tale’s URL, your email and app-password. Choose vendor `other`. Enter the password through the interactive prompt. Follow [rclone’s WebDAV guide](https://rclone.org/webdav/) to list files and copy a small test directory before a larger transfer.
## Verify a small transfer
Open or download a document you can already read in Tale. If your role permits writes, upload a small uniquely named text file in a test folder. Confirm its name and content in **Knowledge > Documents**, then check its indexing status before expecting it in search.
A WebDAV upload follows document permissions and indexing rules; the source is recorded as `webdav`. A successful file transfer does not mean indexing has finished. If a project file seems missing from the mount, open that project’s Knowledge tab instead.
## Handle locks and deleted files
A compatible editor can lock a file while editing. A conflicting write receives **423 Locked**; finish or close the other editing session rather than repeatedly overwriting. Revoking an app-password also releases locks held by that credential.
The `.trash/` area lists soft-deleted documents read-only. Download a retained file if you need to inspect it; use Tale’s UI to restore it. You cannot use this area to retrieve a file that has already been permanently removed. A file you upload, overwrite, copy or delete over WebDAV leaves a row in the audit log under **Settings > Governance > Logs** in your name; a deletion shows as a document moved to trash.
## Revoke or repair a connection
Use **Revoke** on the password’s row and confirm. Subsequent requests with it are rejected, while other app-passwords remain usable. Revocation cannot be undone; update the client with a new password if needed. Generating and revoking an app-password each leave a row in the audit log under **Settings > Governance > Logs**.
Repeated sign-in prompts usually warrant checking the exact URL, organization membership and whether the password was revoked. A permission refusal after authentication is different from a wrong password. Use the [WebDAV API reference](https://docs.tale.dev/develop/webdav-api) for status codes and protocol diagnostics, or [API keys](https://docs.tale.dev/platform/admin/api-keys) for software that needs the REST API instead.
# Developer
Source: https://docs.tale.dev/platform/developer/overview
As a Developer, configure the technical connections and automations that support your team’s work. You have content-editing capabilities as well as access to technical settings such as providers, connectors, and API credentials. Member administration remains with Owners and Admins.
## Choose a connection or workflow
Authorize a script or service to call Tale and plan for rotation and revocation.
Let an external client discover and use Tale’s exposed tools.
Add a credential, choose its default, and recover expired authorization.
Start from a blank workflow or import a package, then test and deploy a version.
## Work from a concrete boundary
For incoming requests, start with the [API reference](https://docs.tale.dev/develop/api-reference) or [webhooks](https://docs.tale.dev/develop/webhooks). For an agent calling another system, prefer a supported connector; direct credentials in a sandbox require careful scoping. [Project agents](https://docs.tale.dev/platform/projects/project-agents) explains equipment. Deployment files and environment variables belong in the [self-hosted configuration guide](https://docs.tale.dev/self-hosted/configuration/environment-reference).
# Editor
Source: https://docs.tale.dev/platform/editor/overview
As an Editor, keep shared information useful: upload documents, correct knowledge entries, and maintain records. You can work on projects where you have edit access. Begin with one source and a question it should answer, then expand the collection as the team’s needs become clear.
## Choose a content task
Choose files, short facts, websites, or structured records for the information.
Bring files and instructions together and ask a question in project chat.
Record acceptance criteria, ownership, progress, and review.
Create and share skills for the agents that need a repeatable method.
## Know when to involve a Developer
The Editor role does not grant workflow authoring or connector administration; those resources are read-only. A Developer, Admin, or Owner handles automation changes and technical credentials. Project-agent management also requires project edit access and working provider and sandbox setup. Check [Members and roles](https://docs.tale.dev/platform/admin/members-and-roles) before starting a guide that needs additional permissions.
# Platform
Source: https://docs.tale.dev/platform
Use these guides to work in Tale, whether your organization uses Cloud or runs its own deployment. Start with a question in a chat, keep recurring work in a project, or follow a specific feature guide when you need to change a setting.
## Move between sections {#navigation}
On a computer, the rail along the left edge shows the sections as icons: **Home**, **Knowledge**, and **Automations**, with **Settings**, your notifications, and your profile menu at its foot. Point to an icon to see its name. On a phone, the tab bar at the bottom offers **Home**, **Knowledge**, **Automations**, and **Settings**. **Automations** shows only for Owners, Admins, and Developers, who build automations; Members and Editors don't see it.
On a phone, navigation sits in a rounded capsule floating above the page. Content scrolls behind it, while message fields and page actions stay above it. The capsule hides when the on-screen keyboard opens and returns when you close the keyboard.
As you scroll down, the navigation becomes smaller and moves slightly lower, showing just the icons. Scroll up to bring back the full bar and labels. Every destination stays available in both sizes.
A section always opens on its own first page, whatever you did there last, so the same choice leads to the same place every time. For example, open an automation's **Runs** tab, switch to **Home**, then choose **Automations**: the automation list opens, not the tab you left. On a computer, Home is the one section that picks up where you were: it reopens the chat you last read, or a new chat if you have none, and choosing **Home** again while you are there starts a new chat. The shortcut **⌥⌘N** on a Mac, or **Alt+Ctrl+N** on Windows or Linux, also starts a new chat.
**Settings** lists its pages in a panel beside the page, and the page header names the page you opened; on a phone, Settings starts from a list of its pages. **Knowledge** shows its pages as tabs under its header.
| You want to… | Do this |
| --- | --- |
| Open another section | Choose that section in the rail or, on a phone, in the tab bar. |
| Start a new chat | On a computer, choose **New chat** in Home, or choose **Home** while you are already there. On a phone, open **Home**, choose **Chats**, then **New chat**. |
| Open a project | On a computer, choose the project under **Projects** in Home. On a phone, choose the project, or choose **Open project** from its menu. |
| Return to the project list | Choose **All projects** in Home, or use the **Projects** breadcrumb above the project. |
| Return to the Documents list | Choose **Knowledge**. |
Bookmarks and shared links to a particular project, task, or document still open that destination.
### Find your work in Home {#home}
Home keeps your chats, tasks, projects, and customer conversations together. On a computer, the Home panel stays beside every chat, task, project, and inbox page. On a phone, **Home** opens the same list as a screen of its own. A chat, task, or conversation you open from it has a single header: a back arrow that returns to the list, its title, and its actions. Your profile menu stays at the top of the Home screen and of Settings.
On desktop, drag the Home panel’s right edge to adjust its width. You can also focus the divider with Tab and press Left or Right. The width is remembered for this organization in your browser.
At the top of the panel, **All**, **Chats**, **Tasks**, and **Inbox** switch what the list shows, and **New chat**, the pencil beside them, starts a chat; its tooltip shows the shortcut. **Inbox** appears only when your organization has an inbox: a deployed mail-sync automation, or an API app that has synced a conversation.
**Projects** lists every project you can open. Choose a project to open its page with the task board, **General**, **Chats**, **Knowledge**, and **Agents**. The two icons beside the **Projects** heading are **All projects**, which opens the full project list, and **New project**. Drag a chat onto a project to file it there. A project's menu offers **New chat** and **Pin project**.
Below the projects, one list groups your work under **Pinned**, **Today**, **Yesterday**, **Previous 7 days**, and **Earlier**:
- Your chats.
- The open tasks assigned to you or waiting for your review, from every project you can read.
- In **All**, the open inbox conversations you can see.
An empty **Tasks** view offers **All projects**, which opens the project list. On a computer, an empty **All** or **Chats** view also offers **New chat**. If your chats, tasks, or conversations cannot be loaded, the list keeps what did load and says what is missing, instead of showing an empty view. **Try again** asks for it again.
On a phone, the same list differs in three ways:
- **New chat** is at the top of the **Chats** view, not in the header. Home has no **New project** button, and a project's menu offers **Open project** and **Pin project**.
- Choosing a project under **Projects** narrows **All**, **Chats** and **Tasks** to that project's chats or open tasks, highlighting the selected project row. The narrowing stays on when you switch between these views. Tapping the project row again removes the filter. **Inbox** is never narrowed by project.
- **All projects** is a labelled link beside the **Projects** heading, not an icon.
Each row starts with a chat bubble, a colored circle for the task's status, or the customer's initials. The title follows with how long ago the item last changed, and one line of context below: the chat's project, the task's key and status such as `WEB-2` **In review** or **Waiting for your review**, or the customer and their latest message. A dot in the accent color marks an unread chat or conversation and a task waiting for your review. **Draft** with a pencil at the start of the context line marks a chat, task, or conversation holding text you typed but have not sent, except the one you have open. Drafts stay in the browser you typed them in. A chat's menu offers **Pin chat**, **Mark as read** or **Mark as unread**, **Rename**, **Move to project…**, **Share**, **Stop sharing** for a shared chat, **Archive**, and **Delete**. Archived chats move to **Archived** at the bottom of the Chats view.
The **Inbox** view lists the conversations of one status: choose **Open**, **Closed**, **Spam**, or **Archived** in its status menu. It also offers **New email**, a search field, and a **Filter** button for **Assignee**, **Read status**, and **Channel**. To act on several conversations at once, point to a conversation's initials and tick the checkbox that appears. The bar above the list then offers **Send messages**, **Close**, and **Mark as spam** for open conversations, or **Reopen** for closed and spam ones, together with **Archive** or **Unarchive** and **Clear selection**.
The checkbox at the start of the bar ticks the whole list. While only some conversations are ticked it shows a dash, and ticking it then selects every conversation the list shows, apart from those a search or filter hides. A selection of more than 200 conversations goes out in parts, and the Inbox counts them off as they go. The message that follows says how many conversations changed and how many failed, with the reason for the first failure when there is one. Those that failed stay ticked, so you can try again. If the list cannot be loaded, it reads **Couldn't load conversations** with **Try again**, rather than showing an empty status.

A chat, task, or conversation opens under a header with its icon, its title, one line of context, and its actions. **Hide sidebar** at the start of that header folds the Home panel away for more room, and **Show sidebar** brings it back; the button's tooltip shows the shortcut. A project's page offers the same button at the start of its header. In a conversation, the first action, **Copy link**, copies a link that opens the same conversation for a teammate. Owners and Admins assign the conversation from its header, to a person, a team, or one of each. Choosing the current person or team again clears it, as do **Unassign** and **Remove team** at the end of the list; the other assignment stays as it is.
Images in an email appear inside the message. Tale fetches the pictures hosted on the sender's servers for you, so the sender sees neither your IP address nor when you read the message. A picture larger than 10 MB, or one in SVG format, does not appear.
### Keyboard shortcuts {#shortcuts}
On a computer, you can also move through Home and open search from the keyboard:
| Mac | Windows or Linux | What it does |
| --- | --- | --- |
| **⌥⌘N** | **Alt+Ctrl+N** | Starts a new chat from any page. |
| **⌘K** | **Ctrl+K** | Opens search from any page, as the magnifying glass in the rail does. |
| **⌘\\** | **Ctrl+\\** | Hides or shows the Home panel on a chat, a task, or an open conversation. |
| **⌥↑** or **⌥↓** | **Alt+↑** or **Alt+↓** | Opens the previous or next chat, task, or conversation in the current view, even while the panel is hidden. |
| **↑** or **↓** | **↑** or **↓** | Moves to the row above or below once a row in the Home panel has keyboard focus. |
When nothing from the list is open, **⌥↓** (**Alt+↓**) opens its first item and **⌥↑** (**Alt+↑**) its last. In a text field, these keys keep their usual meaning. Once a row has keyboard focus, the **Home** and **End** keys jump to the first and last row of its list, and **Enter** opens the focused row.
## Choose a feature
Ask questions, attach files, check sources, compare models, and share a conversation.
Keep files, instructions, personal and shared chats, and tasks together.
Configure agents to work on project tasks with a chosen harness, model, and equipment.
Build a repeatable process, test it, deploy a version, and inspect its runs.
Maintain documents, short facts, public websites, contacts, and products.
Review an automation’s proposed operation before allowing it to proceed.
Create reusable instructions and share them with teams or the organization.
Understand model capabilities, availability, and selection.
Connect external services and understand which actions agents and automations can use.
## Find your starting point
Your role controls which actions are available. Teams and project access decide which resources you can reach. Follow the route closest to your work; an administrator can explain a missing action.
Ask, read shared knowledge, and organize your personal settings.
Maintain shared content and work with the projects you can edit.
Build automations and connect code, clients, and external services.
Set up people, providers, infrastructure, and organization policies.
# Add websites to knowledge
Source: https://docs.tale.dev/platform/knowledge/crawling
Add a website when your team needs to ask about public content that changes over time. Tale fetches the selected pages and indexes their readable text for knowledge search. You need Editor permissions or higher to manage website sources. Pages behind a login need another import route, such as [Documents](https://docs.tale.dev/platform/knowledge/documents).
## Add a website or selected pages
Open **Knowledge > Websites** and click **Add website**. Choose the source type before entering the address:
| Source type | Use it when | What to enter |
| --- | --- | --- |
| **Whole website** | You want content discovered across a domain | A **Domain**, such as `example.com` |
| **URL list** | You need a selected set of pages or public documents | One address per line under **URLs** |
Whole-website mode accepts a URL but uses its hostname; pasting a path does not restrict the crawl to that path. Choose URL list for that purpose. An `http://` address is refused, because the crawler fetches over HTTPS only, and a trailing dot is dropped. The `www` and non-`www` spellings count as the same website, so adding both produces a duplicate warning.
Choose **Scan interval** and **Save**. The default interval is six hours; the available choices range from one hour to thirty days. A newly saved source is picked up by the scheduler. Saving does not mean all pages have already been fetched or indexed.

## Make the pages searchable in chat
Crawling stores a page's text. Chat and knowledge search reach it only once your organization has a working embedding model, because every search first turns the question into a vector. Without one, a source can read **Active** with all its pages indexed while the assistant answers that web-page search is not set up.
**Knowledge > Websites** then shows the notice **Chat can't search these websites yet**; an administrator may see the banner **Knowledge search is off** in its place, above every page. An administrator sets the model under **Settings > Data residency**; operators find the setup details in [Data residency](https://docs.tale.dev/self-hosted/configuration/data-residency). Saving a model starts a scan of every source that was crawled without one and of every source whose last scan failed, so you do not add them again.
## Keep a URL list focused
A URL list fetches only the addresses you provide and follows no additional links. It can contain pages from several websites; Tale groups them into one source per website. A listed `http://` address is accepted and fetched as `https://`, unlike a whole-website `http://` domain, which is refused; a page that serves plaintext only stays out of reach either way. Adding another list for an existing URL-list source adds addresses without dropping the existing ones and updates its scan interval.
Use complete public URLs. Linked PDF and modern Office documents can be indexed when they contain readable text. A download served under a generic or legacy type (`application/octet-stream`, `application/vnd.ms-excel` on an `.xlsx` export) is recognised by its filename and its bytes. Images and scans without extractable text do not become searchable content.
## Understand discovery and refresh
For a whole website, the crawler uses the homepage and published sitemaps, including sitemap indexes and sitemaps declared in `robots.txt`. If usable sitemaps are missing, it follows links within the domain from the homepage. A page absent from both sitemaps and reachable links may be missed; use a URL list when specific coverage matters.
Scans are incremental. Unchanged content is skipped, changed content is indexed again, new pages are added, and removed pages leave the index — and so do pages `robots.txt` has come to disallow. A page that redirects to another address of the same site is indexed once, under the address it lands on. The row's page counts follow the scan as pages land, after discovery and after every stored batch, so the table moves while a scan runs. A URL list follows the same refresh schedule with its fixed selection. There is no separate publish step after successful indexing.
The crawler visits as an anonymous reader. Content that depends on a private session is not made accessible by adding its URL. It identifies itself on every request as `TaleBot/ (+https://docs.tale.dev/platform/knowledge/crawling)`, so a `robots.txt` group can address it by name — `User-agent: TaleBot` — to allow, throttle or refuse it alone.
The crawler applies `robots.txt` `Disallow` rules for the `*` agent on every path a URL can enter by — the sitemaps, the link walk and the links a rendered JavaScript page reveals — and again before every fetch: a page a rule covers is never fetched, and a page a rule added later covers leaves the index on the next scan. The rules do not filter an explicit URL list: a listed address is your instruction. On each content fetch, an HTTP `X-Robots-Tag: noindex` or `none`, or an HTML `` tag, prevents indexing, including for listed URLs, and drops whatever an earlier scan stored of that page. These rules are courtesy, not access control: do not rely on Tale's crawler as an access-control mechanism.
Use HTTPS on the standard port, and register a hostname: addresses with a non-default port, such as `:8001`, and bare IP addresses are rejected — the crawler dials by host name and verifies the certificate against it. Private addresses and redirects into private networks are blocked unless the operator has configured an allowed private-network deployment.
## Work within crawl limits
| Limit | Effect on coverage |
| --- | --- |
| 10,000 tracked URLs per website | A larger site can have undiscovered pages. Use a focused URL list for the material you need. |
| Three minutes for discovery, at most 50 sitemap fetches | Large or slow sitemap collections can be incomplete. |
| 100 MiB (`KNOWLEDGE_CRAWL_DOCUMENT_MAX_BYTES`) and 30 seconds per content fetch | Oversized downloads and slow responses fail (`timeout` for the download and the 20-second browser-render budgets); so does a page behind more than five redirects (`redirect_limit_exceeded`). |
| Five-minute processing budget per batch, up to 200 continuations | Long scans continue in batches. Work already being fetched or rendered can outlast a batch's budget; this is not a guaranteed total scan duration. |
| Five consecutive failures for an automatically discovered URL | The crawler stops scheduling that URL for seven days, then probes it once more. A failure of the render sandbox itself is not counted: when its egress proxy refuses the connection or its browser stops answering, the scan ends with that reason on the source and the next scan retries every page. Listed URLs remain eligible on each scan, and a listed page the site answers 404 for stays in the list with that answer on it. |
There is no configurable page cap, include/exclude path filter, or stop-scan button. A URL list narrows what you request; it does not remove these limits.
## Check what was indexed
The table shows **Status**, the **Indexed** page count, **Scanned**, and **Interval**. Open the source row to inspect its page list, word and chunk counts, and last-crawled times. Expand a page to read its stored text chunks. A failed fetch shows its reason and number of consecutive failures. A page the crawler skipped on purpose — the source asked not to be indexed, the content type has no readable text, or a redirect left the site — reads **Skipped** with its reason and does not count as failed.
| Status | Meaning |
| --- | --- |
| **Scanning** | A scan is in progress; a site you just added starts here. |
| **Active** | A scan completed successfully. Check page-level results for coverage. |
| **Error** | The scan failed, or attempted pages left the source with no stored content. Open the source for its reason. |
| **Deleting** | The source is being removed. |
A scan that was under way when Tale restarted, was updated or stopped without warning continues on its own within a few minutes, with the pages it had not reached yet. Pages that need the browser can follow up to a quarter of an hour later. The source reads **Scanning** throughout.
To scan outside the interval, open the source's row menu or its details and choose **Scan now**. Use it after the site changed, or after a failed scan, which otherwise retries on its own within two hours. The action is offered while the source is neither scanning nor being deleted.
The page view also offers search over indexed content. Try a distinctive phrase from a page before relying on it in chat, then ask a specific question and inspect the citation.
## Resolve a missing page
First check the address, source type, and latest scan time. Then open the source, narrow its page list to **Failed** or **Skipped**, and read the affected page's error.
| Reported issue | What to check or change |
| --- | --- |
| Certificate not trusted | The website operator must fix an expired, self-signed, mismatched, or otherwise untrusted TLS certificate. Repeated scans do not repair it. |
| Private address, refused redirect, or invalid URL | Use the intended public HTTPS address. Ask your operator about approved internal sources if needed. |
| HTTP error, network failure, or timeout | Open the original page and check availability. A later scan can recover after the source service is repaired. |
| Response too large | Publish a smaller document or split the source; the fetch limit is 100 MiB unless your operator set `KNOWLEDGE_CRAWL_DOCUMENT_MAX_BYTES`. |
| Source requests no indexing | The response sends `X-Robots-Tag: noindex` or `none`, or the page carries ``. The source owner must change that directive before Tale can index it. |
| Unsupported content or no readable text | JSON/XML endpoints, binary downloads, images, or scans may provide no supported page text. Supply an HTML page or a supported document with extractable text. |
| Rendering or document extraction failed | Check that the public page loads and the original document opens. Repair or re-export the source if it is damaged. |
| The assistant says web-page search is not set up | The organization has no working embedding model. An administrator sets one under **Settings > Data residency**; Tale then scans the affected sources again. |
| **Error** with "The embedding model couldn't process the pages." | The embedding provider refused or failed the request: a rejected credential, an exhausted balance, a rate limit that held through every retry, or an outage. The source's details say which, with the provider's own reply beneath. An administrator repairs the model under **Settings > Data residency** or its credential under **Settings > AI providers**; saving either one scans the affected sources again. After an outage on the provider's side, choose **Scan now** once it is back. |
| **Error** with "Scanning didn't run." | The crawler's browser could not start, so no page was rendered; its report is under **Technical details** in the source's details. The address is not the cause: ask your operator to check the sandbox service, then choose **Scan now**. |
A successful later fetch clears the previous error. A failed refresh can leave an earlier indexed copy available: **Active** and an indexed count do not prove every page is up to date. Compare the stored chunks and last-crawled information with the original before relying on a recent change.
If the source shows **Paused**, repeated failures to reach the knowledge database stopped scans. Ask an administrator to repair the connection under **Settings > Data residency**, then use **Resume scanning**.
# Documents
Source: https://docs.tale.dev/platform/knowledge/documents
Use **Knowledge > Documents** for files that belong in the shared library: policies, guides, reports, and supporting evidence. Members read documents within their access; Editors and higher roles can upload and manage them. For material that belongs to one project, use that project's [Knowledge tab](https://docs.tale.dev/platform/projects/manage-files).

## Upload from your device
1. Open **Knowledge > Documents** and the folder where the files should go. Use **New folder** if you need one.
2. Choose **Upload documents > From your device** and select the files.
3. Wait for the upload to finish, then find each row in the table.
4. Open a document to check its preview and details. Check the **RAG status** before asking the assistant about its content.
Use a descriptive filename and include a date or revision when it helps distinguish sources. Uploading another file with the same name creates a separate document; it does not replace the existing one.
## Understand upload and search support
A stored file and a searchable file are different states. Tale needs to extract text before it can index a document for knowledge search.
| Format | What to expect |
| --- | --- |
| PDF with embedded text, `.docx`, `.xlsx`, `.pptx`, `.odt`, CSV, plain text | Supported for text extraction and indexing. Check the result for the particular file. |
| Legacy Office `.doc`, `.xls`, `.ppt` | Can be stored and downloaded; convert to a modern format for indexing. |
| Images such as JPG, PNG, GIF, WEBP | Can be stored and downloaded; the knowledge index does not extract text from them. |
| Microsoft Loop files (`.loop`) imported from Microsoft 365 | Can be imported and downloaded; Tale cannot extract their text, so their status is **Not supported**. |
| Scanned PDF without readable text | Supply an OCR-processed or text version if search needs its content. |
Unsupported formats are not made searchable by repeatedly reindexing. For a question about an image, see [Chat attachments](https://docs.tale.dev/platform/chat/attachments), where an available vision model may read it directly.
## Read the indexing status
| Status | What it means and what to do |
| --- | --- |
| **Queued** | Waiting for an indexing slot. A busy library processes files in batches. |
| **Indexing** | Text is being prepared for search. Wait before testing the source. |
| **Indexed** | Indexing completed. Test a specific question and open its citation. |
| **Needs reindex** | The index is stale. Use **Retry indexing** beside the status. |
| **Failed** | Inspect the error, resolve its cause, then retry. |
| **Not supported** | These file contents cannot be indexed: the format may be unsupported, the text empty or unreadable, or the PDF damaged. Open the badge for the cause. |
| **Not indexed** | No completed index is available. Check the file and start indexing where offered. |
Interrupted jobs recover in the background or report a failure with a retry option. If a status does not progress, give an administrator the document name and error. They can check indexing services and the embedding configuration. Failed and unsupported files still use storage until removed.
## Recover from an indexing problem
Click **Failed** or **Not supported** to read the explanation. The next action depends on the cause, not just the filename extension.
| Cause | Next action |
| --- | --- |
| Unsupported format or image | Convert to a supported document with readable text. Uploading an image alone does not run OCR for knowledge search. |
| Empty text or scanned PDF without a text layer | Add the missing content or supply an OCR-processed version. Whitespace alone is also empty. |
| Binary contents behind a text extension | Export readable text, preferably UTF-8. Renaming a binary file to `.txt` does not convert it. |
| PDF cannot be parsed | Check that the original opens, remove password protection where permitted, or export a new PDF. A corrupt Office file may instead report a general indexing failure; check the original before repeatedly retrying. |
| A secret or personal-data policy blocks indexing | Remove the credential from the source, or ask an administrator to review the reported policy restriction. Then upload the corrected material or retry after the configuration is repaired. |
| Embedding model missing or provider account refused | An administrator must configure the model under **Settings > Data residency**, or repair the provider key, model access, plan, or balance. Then retry. |
| Embedding credential missing or unusable | The credential the embedding model uses was deleted or disabled, its provider has no default credential left (**Settings > Data residency** shows **Credential missing**), or its secret cannot be read: it was stored under an earlier encryption key, or it names an environment variable the server does not set. An administrator adds or repairs the credential under **Settings > AI providers**, or chooses another one for the embedding model. Either save puts the affected documents back in the queue. After a fix on the server itself, such as setting the environment variable, use **Retry indexing**. |
| Temporary provider or indexing-service failure | Background jobs retry transient failures. If the error persists, give an administrator the document name and error; after repair, use **Retry indexing**. |
| Search index rebuilding or repair failed | Rebuilding can recover automatically. A failed repair needs the operator to repair or restore the knowledge database before retrying. |
**Not supported** has no retry action: another attempt with the same bytes cannot fix the cause. Failed files can also require a source or configuration change before a retry helps. Applications can distinguish these cases using `indexing.errorCode`; the [API reference](https://docs.tale.dev/develop/api-reference) lists the stable codes.

## If the documents don't load
Documents lists folders and documents from two separate reads. When the documents cannot be loaded, Tale doesn't pass the folders off as everything there is:
- The folders stay listed, and you can still open them. A notice above the table says the documents couldn't be loaded and offers **Try again**, which repeats the read without reloading the page.
- The count below the table says it shows only the first items and that the rest couldn't be loaded. A search covers only what is listed.
- If documents were already listed when a refresh failed, they stay, and the notice says the list may be incomplete or out of date.
- While you retry, the rows stay on screen, and your search, filters and any open dialog are kept.
- If nothing at all can be loaded, the table shows the error and **Try again** instead of the empty state.
A folder that really holds no documents lists its subfolders without a notice. If **Try again** keeps failing, ask an administrator to check that Tale's services are running.
## Choose who can read it
Library documents default to **Organization-wide**. Use **Assign team** in the row menu to restrict a document to the chosen teams: members of any of those teams can read it, and Owners and Admins always can. Unless you are an Owner or Admin, you can only choose teams you belong to. A document inside a team folder takes the folder's teams and cannot name a team outside them, and moving a document into such a folder applies the folder's teams. These restrictions also apply to knowledge retrieval; an agent cannot make an inaccessible document visible through search.
The library root shows folders and documents that have not been filed in a folder. Open a folder to see its contents; a document inside it does not also appear as a file row at the root.
Folders organize the library; to rename one, use **Rename** in its row menu. A synced folder, the folders inside it and the folders that contain it keep their names, because each sync rebuilds that path. Check the **Teams** cell for access and the **Source** column for where a file came from. The **Teams** filter narrows the list to **Organization-wide** items, **My teams** (anything one of your teams can see), or a team by name; the selection is part of the page address, so a filtered list can be bookmarked. Project files are a separate scope and do not appear in this library. See [Knowledge](https://docs.tale.dev/platform/knowledge/overview) when deciding where to keep a source.
## Import from Microsoft 365 or Google Drive
Choose **From Microsoft 365** or **From Google Drive** under **Upload documents**. On first use, connect your account and authorize the import. If Tale reports that import is not configured, an administrator must set up the service under [Connectors](https://docs.tale.dev/platform/admin/connectors) before you can continue.
Select files or folders, then choose the import mode:
| Mode | Result |
| --- | --- |
| **One-time import** | Copies the selection once and preserves its folder structure. Later source changes do not update the copy. |
| **Sync import** | Keeps the supported selection current. New files arrive on a later sync; changed files reindex; deleted source files are removed from the mirror. |
In the library, the **Source** column shows where each file came from. An imported file carries the OneDrive, SharePoint, or Google Drive logo. Circling arrows beside the logo mean a sync import keeps the file current, and they appear on the synced folder itself too; the same arrows struck through mean a one-time import. Uploads, files an agent or automation wrote, knowledge entries, and API imports each show their own icon. Point to an icon to read the source in words.
Starting a folder sync can also reorganize an earlier import. If the same source file is already in Tale, the sync adopts that document and moves it into the matching sync folder, even when its content has not changed. This is a match to the source file, not merely to its name. A sync without a destination folder keeps the existing placement.
For Microsoft 365, choose **My OneDrive** or **SharePoint Sites**. Sync is available for personal OneDrive folders; SharePoint selections import once. For Google Drive, select from My Drive. Native Google Docs, Sheets, and Slides are skipped: export them to PDF or Office formats first.
If a folder is too large to list completely, Tale refuses that import. Select smaller subfolders or use sync where supported. If the selected source folder or file is deleted, its mirror is removed and the sync ends.
When an import ends, a notice reports the result. **Import completed** counts the files this import brought in together with those an earlier import had already brought in unchanged. If some files could not be imported, a warning such as **Imported 3 of 4 files** appears instead. When the first file that failed has a reason to show, such as a file larger than the upload limit, the warning names that file and the reason. If no file could be imported, the notice reads **Import failed**. If the picker is still open, import again to retry the files that failed. Files already imported are skipped.
If access to your Microsoft 365 or Google Drive account ends during an import, for example because the connection was removed or expired, Tale opens **Reconnect Microsoft 365** or **Reconnect Google Drive** instead of a notice while the import picker is still open. The dialog says how many files were imported before access ended; those files are kept. Reconnect your account, then import the same files or folders again to bring in the rest. Files already imported are not imported twice.
Closing the picker does not cancel an import already running. Its result appears in a notice without closing a picker you opened later or changing your new selection. If access ended, open the import again to reconnect your account.
A sync runs about every 15 minutes, under the account of the member who set it up. A file added at the source shows up in its folder within that window and then indexes like an upload. When a run cannot reach the source, the folder row's **Source** cell replaces the circling arrows with a red warning sign whose tooltip reads **Sync failed**, or with a red unplugged plug reading **Reconnect needed** when that member's Microsoft 365 or Google Drive connection has expired. Select the icon to see the cause, when the failures began, and whose account the sync uses; the files synced so far stay in place. That member is also notified in the bell and by email: at once for an expired connection, otherwise once the sync has been failing for an hour. Reconnecting the account, which the dialog offers to that member, resumes the sync on its next run. Any member who can import documents can instead start a new sync import of the same item to run it under their own account. The notice clears with the next successful run.
To keep the imported files without further updates, use **Stop syncing** on the file or folder row. Deleting the imported item also stops its sync. These actions leave the originals in OneDrive or Google Drive untouched. **Disconnect Google Drive** in the import dialog revokes that connection; reconnect when you need to import again.
## Revising a controlled document
Use a controlled document when approval must stay tied to the exact file that a reviewer saw. Replacing its draft updates the existing record; uploading another file with the same name still creates a separate document.
For a regular upload, open the row menu and click **Mark as controlled**. It becomes `v1 · Draft`. An approved record offers both **Replace file** and **New revision**: use **New revision** only when you need the next draft without replacing its file.
Open the draft or approved record's row menu and click **Replace file**, then choose one file in the same format. A draft keeps its current revision. For an approved record, the dialog preserves approved vN and opens draft vN+1 only after the replacement succeeds; cancelling or a failed upload leaves vN approved. A legal hold blocks either path.

Open the document preview and confirm that it shows the replacement. Then open the row menu and click **Submit for review**. The picker offers only members who can actually open the document — a project file needs project edit access — and never yourself: only the reviewer you name can approve or request changes, so every review is a second pair of eyes.
The draft freezes while the reviewer decides on that exact file; the reviewer is notified in the bell and by email, and the decision comes back to you the same way — a request for changes carries the reviewer's feedback, which the submit dialog also shows before your next attempt.
If the reviewer can no longer decide — they left the organization, were disabled, or lost access to the document — open the row menu and click **Change reviewer**: the pending request moves to the member you name, and the record stays frozen on the same file.
## Delete with the contents in mind
**Delete** removes the document and its indexed content. The confirmation explains the impact; keep a copy if you will need the file later. Re-uploading creates a new document.
Deleting a folder permanently deletes its files and subfolders. For a synced folder, it also removes the sync configuration and history. The originals in Microsoft 365 or Google Drive remain untouched.
A controlled document with an approved version is protected from deletion, including while a later draft is being prepared. Its menu shows **Protected controlled record**. A folder containing such a record cannot be deleted either. Legal holds can also block changes or deletion; ask an administrator to check the specific restriction rather than uploading duplicates to work around it.
# Knowledge entries
Source: https://docs.tale.dev/platform/knowledge/knowledge-entries
Use a knowledge entry for a short fact that colleagues should be able to find again: support hours, a return window, or the owner of a process. Each entry has a topic and a body. Choose a [document](https://docs.tale.dev/platform/knowledge/documents) when the source is a whole policy or report, and [structured data](https://docs.tale.dev/platform/knowledge/structured-data) when named fields and exact values matter.
Members can read entries. Creating, editing, and deleting them requires the Editor role or higher. Entries belong to the organization's shared knowledge; do not use one for a personal note or a fact intended only for a particular project.
## Add a fact
Go to **Knowledge > Knowledge entries** and click **Add entry**. If the action is missing, ask an administrator to check your role.
Enter a **Topic** such as `Support response target`. Use a name you would keep even if the answer changes. The topic can contain up to 120 characters and must be unique; edit the existing entry when Tale reports a duplicate.
In **Content**, state the fact, its scope, and any conditions. Markdown is supported, up to 8,000 characters. For example:
```markdown
Support aims to send a first response within 45 minutes during business
hours: Monday–Friday, 09:00–17:00 CET. This is a response target, not a
resolution deadline. Owner: Support Operations.
```
Avoid relative dates such as “next Friday” or references such as “the policy above.” An entry needs to make sense when retrieved on its own.
Click **Save**. The entry appears in the table with its topic, content, source (**Manual** for the form, **Chat** for a fact the assistant captured, **API** for one an integration wrote over REST), indexing status, and update time. Open it to read the full content. Indexing happens in the background; saving the row does not mean search is already using it.

## Correct an existing fact
Open the entry's row menu, choose **Edit**, change the content, and **Save**. Editing creates a new current version and queues its updated text for indexing. There is one current entry per topic, so correcting the existing fact avoids competing answers.
Open the entry's details to inspect **Version history** after a correction. Previous versions record what changed and when they were replaced; they are not additional current facts. An entry that was never edited says it has no earlier versions yet. The **Version ID** shown in the details belongs to the current version and changes with every edit; the topic identifies the entry across versions. An application can also create or update entries through the [REST API](https://docs.tale.dev/develop/api-reference).
When a chat uncovers a useful fact, verify it against the source, then add or edit an entry yourself. Chat does not automatically save facts to the organization's knowledge base.
## Remove an obsolete entry
Use **Delete** in its row menu and read the confirmation. Deletion removes the entry and its version chain from this view and makes its backing document unavailable to knowledge retrieval. Keep a copy before deleting if you still need the text; use **Edit** for a correction instead.
## If the fact is missing from an answer
Check the current entry before changing the prompt. Has it been saved, is its indexing complete, and does the question use a clear topic? If indexing failed, use the retry control after resolving the reported cause. Repeated failures need an administrator to check the organization's embedding configuration and indexing services.
Ask the assistant to cite the source, then open that source and compare it with the entry. A plausible answer alone does not establish that the latest fact was used. [Documents](https://docs.tale.dev/platform/knowledge/documents) explains the shared indexing states in more detail.
## If the list or the history doesn't load
When a read fails, Tale says so where the content belongs. It never shows the failure as an empty library or as an entry without history. **Try again** repeats the read without reloading the page:
- If no entries can be loaded, the table shows the error and **Try again** instead of the empty state.
- If entries are already on screen, they stay, and a notice above the table says the list may be incomplete or out of date. Your search and selected rows are kept while you retry. When further entries could not be loaded, the count below the table says so, and a search covers only the entries that loaded.
- In an entry's details, if **Version history** cannot be loaded, the section says so and offers **Try again**. The current content stays readable.
If **Try again** keeps failing, ask an administrator to check that Tale's services are running.
# Knowledge
Source: https://docs.tale.dev/platform/knowledge/overview
Knowledge is your organization's shared library. Add documents, short facts, public websites, contacts, and products so people and agents can work from the same sources. Start with the information you need to answer a real question; a small, current collection is easier to trust than an unreviewed archive.

## Choose the right place
| You have | Use | Why |
| --- | --- | --- |
| A policy, guide, spreadsheet, or report | **Documents** | Keep the original file and retrieve passages from supported formats. |
| A short fact such as support hours | **Knowledge entries** | Give it a stable topic and update one current answer. |
| Public pages that change over time | **Websites** | Crawl a whole website or a chosen URL list on a schedule. |
| People, organizations, and their contact details | **Contacts** | Keep names and other fields as structured records. |
| Items with product details | **Products** | Look up the record's fields instead of searching a paragraph. |
| Reference files for one piece of work | A project's **Knowledge** tab | Keep them within that project's access and chat context. |
Members can read content within their access. Editors and higher roles curate the shared library. Open a row, or choose **View** in its row menu, to see its details. If you can change a source, the same menu offers the actions that change it, such as **Delete**; contacts, products, websites, and knowledge entries also offer **Edit** there and in their details. The [structured-data guide](https://docs.tale.dev/platform/knowledge/structured-data) helps when a spreadsheet could be either a source document or a set of records.
## Make a source useful for answers
Uploading or saving is the first step. Documents, entries, and website pages also need to be **indexed**: Tale extracts their text and prepares it for search. Check the status before asking about newly added content. A file can remain downloadable even when its format cannot be indexed.
Use a clear title, include dates and scope in the content, and remove or correct outdated guidance. If two sources disagree, tell the assistant which one is authoritative and verify its citation. Adding more files does not resolve a contradiction between them.
Test a new source with a question whose answer you already know: “What are our support hours? Cite the source.” Open the cited source and compare the answer. This checks usefulness more directly than asking for a general summary of everything in the library.
## Understand access and search
Organization chat can search the shared library within your permissions. Project chat can also search that project's files and uses its saved instructions; it does not search another project's files. Project agents need the corresponding platform tools in their equipment.
Team restrictions continue to apply during retrieval. A file you can see in one workspace may therefore be absent from a different project's context. Use [project files](https://docs.tale.dev/platform/projects/manage-files) for project-specific material and document team access for shared library files.
If all searches fail, ask an administrator to check the embedding model and knowledge connection in **Settings > Data residency**. Operators can find setup details in [Data residency](https://docs.tale.dev/self-hosted/configuration/data-residency).
## Maintain each source
Upload, import, check indexing, and maintain approved document revisions.
Add a concise fact, correct it, and read its version history.
Choose pages to crawl, set the interval, and investigate missing pages.
Choose records for exact fields and documents for supporting explanations.
# Choose documents or structured records
Source: https://docs.tale.dev/platform/knowledge/structured-data
Use documents for information that needs paragraphs to explain, such as a contract or a meeting note. Use structured records for information that belongs in named fields, such as a contact’s email address or a product’s identifier. Most teams need both: the record identifies the thing, and the documents explain its history or context.
## Choose a home for the information
| Information | Put it in | Why |
| --- | --- | --- |
| A policy, contract, manual, or meeting note | **Documents** | Tale searches the text and retrieves relevant passages. |
| A short fact that needs its own update history | **Knowledge entries** | One topic has one current version, with earlier versions retained. |
| A person or organization you work with | **Contacts** | Named fields keep details together in a record you can update. |
| A product and its attributes | **Products** | Product details stay in fields instead of being buried in a file. |
| Pages on a public website | **Websites** | Tale crawls the pages and updates their searchable content on a schedule. |
| Reference files for one project | The project’s **Knowledge** tab | Access follows the project, and project chats can retrieve the files. |
The way information is stored affects how it is retrieved. Finding a passage in a document does not establish that the whole file was reviewed. Reading a record’s fields gives Tale those values; it does not guarantee that the source is current or that an answer based on it is correct.
## Combine records with supporting documents
Suppose you need to prepare for a call with Acme. Keep its contact details in **Contacts** and its contract and meeting notes in **Documents**, or in the relevant project’s **Knowledge** tab.
Ask the chat assistant to find Acme’s contact record and summarize the open questions from the latest notes. Check the record for the email address and the cited notes for the decisions. If the files belong to a project, start the chat inside that project so it can reach them.
Use the same recognizable company or product name in record titles and supporting files. Add dates to meeting notes and revision identifiers to policies so you can distinguish a current source from an old one.
## Create a contact
You need an Editor role or higher to maintain organization records. Open **Knowledge > Contacts**, choose **Add contact**, then **Manual entry**.
1. Enter the contact’s **Email**. Add a **Name** and **Phone** if useful.
2. Check **Locale**, which starts as `en`, and set the language appropriate to the contact.
3. Choose **Save**. The new row appears in Contacts with its details and added date.
The form holds each field to what Tale stores before it saves: a **Name** of up to 300 characters, a **Phone** of up to 50, a **Locale** of up to 20, and an **Email** with at most 64 characters before the `@`. A value past a limit is named under its field, and nothing is saved until you fix it.
To see only the contacts in one language, choose **Filter** above the Contacts list and pick the language under **Locale**. A language includes its regional variants: **FR** lists contacts whose locale is `fr`, `fr-CH`, or `fr_CA`. A contact without a locale appears only while no language is picked.
You can also add a contact while writing to them. In the **Inbox** view in Home, choose **New email** and type an address into **To**: if no contact carries it, the list offers **Add “…” as a contact**, which opens this same form with the **Email** already filled. Saving makes that contact the recipient, so you never leave the message you are writing.
If the email address already exists, find the existing contact and update it through the row menu instead of creating a duplicate; from **New email**, Tale selects the existing contact for you. Saving a contact creates a record; it does not send that person an email.
You can edit or delete the contacts your team entered or imported here, one at a time from the row menu. To delete several at once, tick their rows and choose **Delete selected**. Contacts that came from an integration, the API or a conversation belong to their source, which would overwrite a change made here: their row menu offers no **Edit** or **Delete**, and their rows have no checkbox.
## Create a product

Open **Knowledge > Products**, choose **Add product**, then **Manual entry**. The form has three stages.
1. Under **Basics**, enter a **Product name**. Add a description and image if they help someone identify the product, then choose **Next**.
2. Under **Pricing & inventory**, set **Price** and **Currency** together. For example, enter `12.50` and select `CHF`; changing the currency does not convert the amount. Add stock and category when relevant, and check **Status**. A new product starts as **Draft**.
3. Under **Review**, check the details and choose **Create**. The product appears in the table with its price, status, and updated date.
Use the row menu to edit a saved product. Keep product names distinct so teammates can identify the right record, and review the price and currency before changing its status.
To add a product image, choose **Upload image** or drop a PNG, JPEG, WebP, GIF, or SVG file into the image area. The limit is 5 MiB. Wait for the preview before moving on. Tale checks the uploaded bytes; if it refuses the file, the message under the image field says why — an unsupported format, a file over the limit, or an SVG that carries scripts or event handlers — so you know whether another file is needed.
The uploaded image stays available after you save and reload the product. Other organization members with product access can view it once the product is saved; the image address requires a signed-in session and is not a public sharing link. To remove it, edit the product, choose **Remove image**, then save. Removing or replacing the image, or deleting the product, removes the upload itself as well, unless another product still shows it.
If you choose **Or paste a URL**, enter a complete public HTTPS address, starting with `https://`. The form names an incomplete address under the field before you can continue. Tale refuses unsafe or disallowed hosts; ask an administrator if you need an internal image source. Images loaded from external addresses follow that source’s access rules.
## Keep access and freshness in view
A record or document is useful only to people who can access it. Check its team scope when a teammate cannot find it. Project files follow project access rather than the document library’s team tags; see [Project files](https://docs.tale.dev/platform/projects/manage-files).
Update the authoritative record when a detail changes. For a revised document, wait until indexing finishes before testing a question against its new text. Website content follows its configured scan interval, so it may lag behind the live page.
## Work with the available record types
The Knowledge area provides Contacts, Products, and Websites. **Settings > Governance > Models** controls AI model access and defaults; it does not create custom record types or database fields.
If the available fields do not fit your material, keep the detail in a document and link the surrounding process to the appropriate record. For programmatic imports and the fields each resource accepts, use the [API reference](https://docs.tale.dev/develop/api-reference).
Read [Documents](https://docs.tale.dev/platform/knowledge/documents) to upload and verify a file, [Knowledge entries](https://docs.tale.dev/platform/knowledge/knowledge-entries) to maintain one fact, or [Crawling](https://docs.tale.dev/platform/knowledge/crawling) to keep public pages searchable.
# Install Tale as an app
Source: https://docs.tale.dev/platform/member/install-as-app
Install Tale if you want a launcher icon and a separate app window. Use the same Tale address and account as in your browser. Installation does not create another organization or copy your chats into a new account.
## Install on a computer or Android
Open your Tale instance in a supported browser. Open the account menu from your name or avatar and choose **Get app**, then accept the browser’s installation prompt. Tale shows this action when the browser makes installation available.
If the action is missing, check the browser’s own menu. Chrome provides **Cast, save, and share > Install page as app** on desktop; wording varies by browser and device. [Chrome’s app guide](https://support.google.com/chrome/answer/9658361?hl=en) covers installation and management.
Open the new icon and confirm that you reach the intended Tale instance. Sign in if asked, then check the organization in the account menu. A browser profile or an installed app may have a different sign-in state.
## Install on iPhone or iPad
In Tale’s account menu, **Get app** opens manual installation instructions on iOS. The browser performs the installation.
In Safari, open Tale, choose **Share > Add to Home Screen**, and confirm **Add**. If **Open as Web App** is shown, leave it enabled for a standalone window. Apple documents the current controls in its [iPhone app installation guide](https://support.apple.com/guide/iphone/open-as-web-app-iphea86e5236/ios).
Other iOS browsers can also offer **Add to Home Screen**. If your browser does not expose it, open the same address in Safari. The installed app’s cookies can become separate from the browser’s after installation, so signing out in one window does not necessarily sign out the other. [WebKit’s web-app behavior](https://webkit.org/blog/14787/webkit-features-in-safari-17-2/) explains this separation.
## Understand what changes
Your organization, permissions and saved content stay on the Tale instance. Installation changes how you launch and display it. It does not install a local model or make server-dependent work available without a connection.
Notifications still depend on browser and operating-system support, permission, and your Tale preferences. Installing the app alone does not grant notification permission. If you change device or reinstall, check those permissions again.
## Remove the installed app
Use the browser or operating system’s **Uninstall** or **Delete App** action. Removing an icon from the macOS Dock only removes that shortcut. In Chrome, use the installed app’s menu or `chrome://apps`; deleting its browser data may also sign you out.
Uninstalling the app does not delete your Tale account, organization or server-side documents. Use [Profile and account](https://docs.tale.dev/platform/member/preferences) for account changes. On a shared device, sign out of both the app and any browser session you used.
## If installation is unavailable
| What you see | What to try |
| --- | --- |
| No **Get app** action | Check the browser’s installation menu and whether the app is already installed. |
| No install option in a private or managed browser | Try a regular window, or ask the device administrator about installation policy. |
| The icon opens the wrong instance | Check the address in the original browser, then install the intended instance. |
| App opens but cannot load content | Check network access and whether the Tale instance is running. |
# Member
Source: https://docs.tale.dev/platform/member/overview
As a Member, use Chat to ask questions and read the knowledge and project content shared with you. Your organization’s setup and team memberships determine the resources you see. You can also create project tasks and put project agents to work on them, maintain your account preferences, and create reusable skills.
## Start your day
Practice with a source, check the facts, and ask a follow-up.
Find documents, facts, and records, and understand why a source may be unavailable.
Learn which files and conversations belong to a project and which chats remain personal.
Edit your profile, check your usage limits, and understand saved preferences and chat management.
Move between your daily work with the [main navigation](https://docs.tale.dev/platform#navigation). Each section opens on its own first page; on a computer, **Home** reopens the chat you last read instead, and choosing it again starts a new chat. [Home](https://docs.tale.dev/platform#home) also lists your projects, the open tasks assigned to you or waiting for your review, and the inbox when your organization has one.
## Hand work to a project agent
When you need a deliverable, such as a summary of several contracts or a translated document, open a project you can access and create a task on its **Tasks** tab. Describe the result and how you will check it, attach the files it depends on, and assign one of the project's agents. **Start agent** puts it to work: the agent reports in the task's comments and moves the task to **In review**. Read the report and open the deliverables, then move the task to **Done** when you accept the result.
You can change the tasks you created and the tasks assigned to you. On other people's tasks you read and comment. Setting up the agents themselves, and the project's settings and files, stays with Editors. [Manage tasks on a project board](https://docs.tale.dev/platform/projects/tasks#who-can-create-and-change-tasks) and [Delegate a task to an agent](https://docs.tale.dev/platform/projects/task-automation) go into the details.
## Follow a notification
A notification link opens the related task, document or conversation. If you need to sign in first, Tale keeps the destination and opens it after sign-in. Your account still needs access to the organization and the linked content. For a private deployment, connect to its required network before opening the link.
## If an action is missing
Members read the shared library; Editors and higher roles maintain its content. In a project, you change the tasks you created or that are assigned to you; its settings, agents, and files stay with Editors. Automations and technical setup require Developer or administrator permissions, so Members and Editors don't see **Automations** at all. Ask for the access needed for your task rather than assuming every visible resource is editable. [Members and roles](https://docs.tale.dev/platform/admin/members-and-roles) explains the differences.
# Manage your account and preferences
Source: https://docs.tale.dev/platform/member/preferences
Your account settings control how teammates recognize you and how you sign in. Your profile menu also lets you switch organization and language and shows the teams you belong to. These controls are available without an administrator role.
## Update the name your teammates see
Open **Settings > Account**. Under **Profile**, edit **Name** and click **Save** in the page header. **Discard** restores the saved value. Your email address is shown as read-only because it identifies the account used to sign in and receive notifications.
The name is visible to teammates and can be at most 100 characters long. It is not a private instruction to the assistant.
## Protect your sign-in
The **Password** section offers **Change password**, or **Set password** for an account that does not yet have one. Follow the password requirements shown in the dialog. They come from your organization's password policy. If you belong to several organizations, your password must meet the requirements of every one of them. Changing the password signs out your sessions, so keep the new password available before confirming. A wrong current password counts toward the same temporary lock as a failed sign-in.
Set up an authenticator under **Two-factor authentication** or add a passkey under **Passkeys**. Store backup codes somewhere you can reach without signing in to Tale. [Two-factor authentication](https://docs.tale.dev/platform/admin/two-factor-authentication) covers setup, recovery, and organization requirements.
## Switch language or workspace
Open the profile menu from your avatar. **Language** changes the interface language. When you belong to several organizations, **Organization** switches the workspace. The **Teams** row names the teams you are in and opens the account page; it switches nothing, because a team is not a workspace.
Check the organization name before changing settings or adding content.
## See your role {#role}
**Settings > Account > Your role** shows your role in this organization, such as Editor or Member. Your role decides what you can do; your teams decide which team work you can see. Admins assign roles under [Members and roles](https://docs.tale.dev/platform/admin/members-and-roles). With single sign-on, your identity provider can also set your role each time you sign in. Owners and Admins see a **Manage members** button beside the section's title.
## See your teams {#teams}
**Settings > Account > Your teams** lists the teams you belong to. Teams decide which team documents, projects, and inbox queues you can see; work shared with the whole organization is visible to you regardless. When you are in no team, the section says so. If your teams can't be loaded, the section says that instead of claiming you are in no team. Choose **Try again** to load them. When a refresh fails, the teams already shown stay, with a note that they may be out of date.
To narrow a list to certain work, use its **Teams** filter: **Organization-wide** shows only items without a team, **My teams** shows items any of your teams can see, and each team is listed by name. The inbox offers an **Assignee** filter behind its search box, listing people and teams together. A filter changes the current view; it does not grant access to another team’s data.
Owners and Admins manage membership under [Teams](https://docs.tale.dev/platform/admin/teams); the section's **Manage teams** button takes them there.
## Set custom instructions for the chat assistant
Open **Settings > Preferences**. **Custom instructions** are standing instructions the chat assistant follows in every reply to you, such as a preferred tone, a default programming language, or how much detail you want. The switch can follow the organization default or store your own choice; the hint under it says which applies. The text field appears while the feature is on. Type your instructions and click **Save** in the page header.

Your instructions never override the organization’s mandatory instructions or a project’s **General > Instructions**; where they conflict, those take precedence. Turning the switch off keeps the text for later without applying it.
## Check your usage limits {#usage-limits}
Open **Settings > Usage** to see how much you have used of each limit your organization applies to you. The page says so when no limit covers you.

- **Your limits** count your own chats, voice output, and agent runs, whichever way you started them; [How usage is counted](https://docs.tale.dev/platform/admin/governance/usage-attribution) explains who a run counts against. When one is reached, you can't start more of that work until it resets: a message you send then is refused with a notice that names the limit, and it stays in the composer.
- **Shared limits** count the usage of everyone they cover, such as a team you belong to or the entire organization, so they can be reached before your own limits.
- **Storage** compares the files you have uploaded with your storage limit. New document uploads are refused once it is reached.
Each usage limit shows the amount used, the limit, and when it resets in your local time. Periods follow UTC: daily limits reset at midnight, weekly limits on Monday, and monthly limits on the first of the month. If an administrator set a warning threshold, the bar turns amber once your usage reaches it. When a banner above the composer warns about a limit, **View usage** opens this page. Administrators also see **Manage limits**, which opens **Governance > Policies & Limits**.
## Archive old chats or sign out
**Settings > Account > Your chats** offers **Archive all chats** and **Delete all chats** for your own chats in the current organization, including project chats. Archiving affects unarchived chats; deleting also includes archived chats and moves them to Trash, where they can be restored during the retention grace period. Chats under legal hold remain unchanged, and chats with a running reply cannot be deleted.
Read the confirmation before proceeding. The result reports how many chats changed and how many could not be changed. Use an individual chat's menu in Home when you only want to organize that conversation. Archived chats stay under **Archived** at the bottom of the Home list, where **Unarchive** in a chat's menu brings it back.
**Log out** in the profile menu ends the current session and returns you to sign-in. Sign out on a shared device when you finish using Tale. For a dedicated app window on your own device, see [Install as app](https://docs.tale.dev/platform/member/install-as-app).
If your session ends while Tale is open, for example because you logged out in another tab, a refused request shows **Your session has ended. Sign in again.**, and a document upload fails with the same sentence and no retry. A project's **Environment** tab you open afterwards shows **Admin access required** instead. One that was already open, or that you open again less than five minutes after it last loaded, still shows its variables, and saving there shows the same sentence. **Settings > Data residency** says **You're not allowed to manage this organization's data residency.** On your organization's pages, whose addresses start with `/dashboard`, Tale checks the session and asks before opening sign-in. Signing in leaves the page, so unsaved changes may be lost. Choose **Stay here** to keep the page and copy any work you need. Further refusals do not reopen the confirmation. On other pages, Tale doesn't ask: reload the page to sign in again.
When you are ready, close any open dialog and choose **Sign in** in the notice that stays at the top of the page, above its header, then confirm. Tale checks again before leaving; if another tab has already restored your session, your page stays open. Otherwise, the sign-in page shows the same session notice. After signing in, you return to the page you were on; copied work can be re-entered, but unsaved drafts are not restored automatically.
# Choose an available model
Source: https://docs.tale.dev/platform/models
The model picker shows what your organization can currently use, not every model a vendor sells. A usable provider credential, its model allowlist and the organization’s access rules all affect the result. An administrator manages these under **Settings > AI providers** and [Content & models](https://docs.tale.dev/platform/admin/governance/content-models).
## Choose automatic or explicit selection
In Chat, **Auto** selects a model for each message using the message’s characteristics, such as length, code and attached documents. It uses a lightweight heuristic, not a second AI request. Inspect a reply’s details to see which model actually answered.
Choose a specific model in the composer when you need to compare results, control the choice or use a model suited to a known task. That selection remains until you change it or return to Auto. [Arena](https://docs.tale.dev/platform/chat/arena-mode) lets you compare two available models with the same prompt.
Project agents and workflow model steps use their configured model. An agent’s picker distinguishes entries from different providers, even when the model ID is the same. Selecting an entry pins that provider/model pair. A model failure is reported rather than silently answered by a different model.
## Understand where the list comes from
Open **Settings > AI providers** to inspect the provider and its offered models. The catalog count describes the provider’s available definitions; it does not establish that your organization has permission or credentials to call all of them.
| Source | How models enter the catalog | When it changes |
| --- | --- | --- |
| Built-in catalog | Model definitions ship with Tale. | With a platform/catalog update. |
| OpenRouter catalog | Tale fetches OpenRouter’s list. | After a fetch or explicit refresh. |
| Provider models endpoint | Tale fetches the provider’s own model list. | After a fetch or explicit refresh. |
| No catalog | Model IDs come from the credential’s allowlist. | When an administrator edits that list. |
Azure OpenAI and Nous Portal use credential-defined model IDs. For Azure, enter your resource’s deployment names, which may differ from public model names. An empty allowlist on a provider with no catalog makes no models available.
## Refresh a fetched catalog
Owners, Admins and Developers can select **Refresh catalogs** in the settings header. Read the result for each provider: it reports the model count or the error that prevented refresh. A failed refresh is not evidence that the provider offers zero models.
Remote catalogs are cached for 24 hours and refreshed on demand when the cache is stale. The button forces a fresh attempt. A failed automatic fetch can continue serving a previous catalog or shipped defaults; a forced refresh reports the failure. A newly published model also needs to pass credential and policy checks. Built-in-only deployments have no remote catalogs to fetch.
## Find why a model is missing
Check these boundaries in order, or give the details to an administrator if you cannot edit settings:
1. Confirm the provider has an enabled, usable credential. A catalog entry by itself is not an account connection.
2. Inspect that credential’s **Model allowlist**. For a catalog provider it restricts the list; for a provider without a catalog it defines the list.
3. Check the model-access rules for the relevant organization, team or user under [Content & models](https://docs.tale.dev/platform/admin/governance/content-models).
4. For a project agent, check that the credential supports its selected [harness](https://docs.tale.dev/platform/agents/harnesses). A subscription may work only with its required runtime, and a model whose tool calls need OpenAI’s Responses API, such as GPT-6.1 Sol, is offered only for Codex.
5. In Chat, a model that only a subscription credential serves is not offered: a subscription runs only in tasks and automations, and the chat model list names the subscriptions it leaves out. Add an API key or environment-variable credential to chat with that model; [AI providers](https://docs.tale.dev/platform/admin/providers#use-subscriptions-in-tasks-not-in-chat) explains why.
If the model is visible but a call fails, read the displayed reason. Expired credentials, provider failure, budget limits and lack of sandbox capacity are different problems. Refreshing a catalog cannot repair all of them. [AI providers](https://docs.tale.dev/platform/admin/providers) covers credentials; [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits) covers spending refusals.
# Triage the project backlog
Source: https://docs.tale.dev/platform/projects/backlog
Use **Backlog** for proposed work the team has not committed to yet. It is a normal task status, shown first in the board and list. A proposal can already have an assignee; assignment alone does not make it active work. A proposal assigned to you still appears among your tasks in [Home](https://docs.tale.dev/platform#home), marked **Backlog**.
## Capture a proposal
Open the project’s **Tasks** and create a task. Choose **Backlog** in the status picker; new tasks otherwise default to **To do**. Give the proposal a title describing the outcome and enough context for someone to decide whether to pursue it.
For example, “Check the mobile checkout error” is easier to triage with the affected page, a reproducible symptom and a screenshot than with a title alone. Leave implementation detail open until the problem is understood.
A project agent with the task-creation tool can also create a proposal. That tool allows initial **Backlog** or **To do** status, not a working or finished status. The same boundary applies when an automation uses the project’s task tools.
## Decide what happens next
| Decision | Action |
| --- | --- |
| More information is needed | Keep **Backlog** and explain the missing information in the description or a comment. |
| The team accepts the work | Set **To do** and choose an assignee. |
| Work has started | Move to **In progress**. |
| The proposal will not be pursued | Set **Cancelled**, preserving the task’s discussion. |
Change status in the task detail or drag the card between board columns. These are the same controls used for other tasks; Backlog has no separate accept or reject workflow.
## Hand the task to an agent
Choose an agent from the same project and make the expected result explicit before clicking **Start agent**. Assigning it does not replace that start action. Use [Task automation](https://docs.tale.dev/platform/projects/task-automation) for prerequisites, progress and review.
## Understand where proposals come from
The shipped **Triage GitHub issues** automation returns a ranked report. It does not create project tasks or fill Backlog automatically. A person or an appropriately equipped agent must turn a selected issue into a task. This keeps the decision to accept work separate from the report that recommends it.
[Manage project tasks](https://docs.tale.dev/platform/projects/tasks) covers task details, comments, dependencies and the rest of the board.
# Project concepts
Source: https://docs.tale.dev/platform/projects/concepts
Use a project when several questions or tasks depend on the same reference material. A project brings together files, instructions, chats, a task board, and task agents. A one-off question can stay in an ordinary chat; a launch, customer handover, or ongoing investigation usually benefits from a project.
## What the project keeps together
| Area | What belongs here |
| --- | --- |
| **General** | Name, description, standing instructions, sharing, and the default task reviewer. |
| **Chats** | Your project conversations and the conversations explicitly shared with the project. |
| **Knowledge** | Reference files organized into folders and scoped to this project. |
| **Tasks** | Work with an owner, status, comments, and a result to review. |
| **Agents** | Named agents configured to work on tasks. |
Project chat starts with the saved project instructions. It can search this project's files together with the organization's accessible knowledge. This gives later conversations a common starting point without copying a brief into every message. Other projects' files are outside that search scope.

## Create an identity people can recognize
**Create project** asks for a name and a **Project key**, with an optional description, icon, and color. The key becomes the prefix of task IDs such as `WR-1` and cannot change after creation. Choose a short, durable abbreviation.
You can revise the name, description, icon, color, and instructions on **General** later. Use **Save** to apply field edits or **Discard** to abandon them. For a complete walkthrough, follow [Use projects](https://docs.tale.dev/tutorials/member/use-projects).
Write instructions for recurring context: what this work covers, which sources take precedence, and how to handle missing information. Put a one-time request in a chat or task description. Project instructions should not become a log of every past decision.
## Check who can open it
New projects default to **Org-wide** access. Choose teams under **Who can see it** when you create a project, or under **Audience** on **General** later. An empty audience means everyone in the organization; otherwise members of any listed team can open the project, and organization administrators always can. Unless you are an Owner or Admin, you can only pick teams you belong to. Removing a team later hides the project from members outside the remaining teams, and the page asks you to confirm that. Sharing is based on teams rather than individual invitations; the project list shows each project's teams in its **Sharing** column and offers a **Teams** filter.
Project files follow project access. They do not appear as ordinary library documents, and a team tag on a library file does not make that file a project attachment. [Manage files](https://docs.tale.dev/platform/projects/manage-files) explains moving documents and how removing a file from a project can widen its audience.
## Keep personal and shared chats distinct
A chat inside a project starts as your own conversation. Other project members do not see it simply because they can open the project. **Chats** separates **Your chats** from **Shared with project**; use **Share with project** when the conversation is ready for colleagues.
Colleagues open a shared chat read-only: they cannot reply, edit a message, or rate an answer. Where you edited a message or retried an answer, they read the version you have selected, not the one it replaced.
Read the messages before sharing, including any sensitive information the answer quotes. Moving a shared chat to a different project, or out of the project, ends its project share. Share it again deliberately if the new audience should read it.
Use **Move to project…** in a chat's actions, or drag the chat onto the project in Home, when an existing conversation belongs with this work. Organization-wide snapshot links are a separate option, described in [Share a chat](https://docs.tale.dev/platform/chat/shared-threads).
## Turn discussion into work
Create a [task](https://docs.tale.dev/platform/projects/tasks) when a decision needs an owner or a result. A teammate can complete it manually, or a configured [project agent](https://docs.tale.dev/platform/projects/project-agents) can work on it. Keep the acceptance criteria in the description so the reviewer can judge the result.
Archive a finished project when you want it out of the active list. An archived project is read-only for everyone — its settings, tasks, files, and agents can be read but not changed — until a project administrator restores it from **General**. Before deleting, read the choice about its contents: detaching leaves files in the library and chats as personal conversations; deleting the contents removes them too. Detaching files can widen access, so choose according to what should remain available.
# Manage project files
Source: https://docs.tale.dev/platform/projects/manage-files
The project’s **Knowledge** tab holds files that its chats can retrieve. Upload a reference once and use it across conversations in that project. You need project edit access to add, organize, or remove files.

## Upload into the right folder
1. Open the project and select **Knowledge**.
2. Select a folder, or leave the root selected.
3. Click **Add file** or drop files onto the upload area.
4. Check that each file appears in the intended folder and finishes indexing.
**New folder** creates a folder at the root. A folder’s **New folder inside** action creates a subfolder. **Upload folder** imports a folder from your device and recreates its structure under the selected location. A folder upload is limited to 200 files and 200 MB; split larger folders and check the report for skipped files.
## Check whether chat can read a file
| Status | Meaning and action |
| --- | --- |
| **Queued** | The file is waiting to be processed. |
| **Indexing…** | Tale is preparing its text for search. |
| **Indexed** | The text is searchable; verify a question against the original file. |
| **Failed** | Open the failure detail when available and use **Retry indexing**. Ask an admin if the failure returns. |
| **Not supported** | These contents cannot be indexed. Supply readable text or a supported format; retrying the same file cannot help. |
| **Not indexed** | The file is stored but not searchable. Use **Index now** when offered, or convert a format without a text extractor. |
An integration can upload a file without indexing it. Such a file is still visible in the tree; a supported plain-text file can be read directly when named, but it will not appear in text search until indexed.
When not every file arrives, the upload report lists the rest with a reason: **Skipped** files failed the type or size check before uploading, and **Not added** files were refused by Tale, which says why. Per-file and storage limits may be restricted further by organization policy. When an upload fails, first try a small supported file. An admin can check [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits), storage, and the embedding model.
## If files or folders don't load
When the tab cannot load the project's files or folders, a notice above the tree says what failed and offers **Try again**. Tale never shows the project as empty because a read failed:
- If only the folders fail to load, every file that loaded stays listed. Files at the root stay in place, and files inside folders appear under **Files inside folders** until the folders load.
- If only the files fail to load, the folders stay visible and the notice says the files are missing.
- If a refresh fails after the tree has loaded, the tree keeps what it showed, and the notice says it may be out of date.
**Try again** repeats only the read that failed, and the tree keeps its expanded and selected folders. If it keeps failing, ask an administrator to check that Tale's services are running.
## Ask about the files from a project chat
Open **Chats** in this project, start a chat, and ask about the file by name or topic. The assistant can retrieve this project’s files and the organization’s Knowledge documents that you can access. It cannot retrieve another project’s files from here.
The organization’s general chat does not search project files. Files also stay out of the organization’s document list and WebDAV library while they belong to the project. Project access determines who can read them; project files do not have separate team tags.
## Replace a controlled file without losing its review history
Uploading another file with the same name creates a separate document; a matching filename is not a revision link. For a file whose approval must remain tied to exact bytes, use the row menu’s **Mark as controlled** action.
A controlled record starts as a draft. **Replace file** updates a draft or opens the next draft from an approved version while preserving that approved version. **Submit for review** freezes the draft for the named reviewer. See [Controlled documents](https://docs.tale.dev/platform/knowledge/documents#revising-a-controlled-document) for the complete lifecycle and reviewer rules.
## Move to Knowledge or delete
**Remove from project** moves the file to the organization’s Knowledge library. It does not delete the file.
Removing a file from the project makes it visible to everyone in the organization. Use this only when you intend to publish it to that wider audience. Read the confirmation before proceeding.
To remove a file entirely, use **Delete** in its row menu and read the deletion confirmation. Folder deletion removes its contained files and subfolders as well as their search entries. These actions cannot be undone through the file tree. Legal holds and protected controlled records can block deletion.
**Delete** is offered for files uploaded here and for files an agent wrote into the project, such as readings or generated reports. A file synced from a connector offers no Delete in the project, because the next sync would restore it; remove it at its source instead.
If a file serves several unrelated projects, consider keeping an appropriately scoped copy in the [Knowledge library](https://docs.tale.dev/platform/knowledge/documents). Avoid maintaining several conflicting copies of the same policy.
# Projects
Source: https://docs.tale.dev/platform/projects/overview
A project keeps the files, instructions, conversations, and tasks for one piece of work together. Use one when context needs to last beyond a single chat or when a result needs an owner and a review. Start with [Use projects](https://docs.tale.dev/tutorials/member/use-projects) to create a project and ask a question about its reference file.

## Find the next step
Learn what the project shares, which chats stay personal, and how teams control access.
Upload and organize files, check indexing, and manage controlled revisions.
Set an owner, reviewer, dates, and acceptance criteria; follow progress on the board.
Choose a harness, model, tools, and instructions for an agent that can take tasks.
Start a task, review the result, request rework, and recover a failed run.
Use Backlog to review ideas before committing them to the team's work.
Every project you can open is listed under **Projects** in [Home](https://docs.tale.dev/platform#home), where **All projects** opens the full list. A project opens on its task board; **General**, **Chats**, **Knowledge**, and **Agents** sit beside the task views. For Owners, Admins, and Developers, a bound automation adds an **Automations** surface; project administrators can configure **Environment**. Installed apps may add further tabs; you do not need them to start with files, chats, and tasks.
# Create and manage project agents
Source: https://docs.tale.dev/platform/projects/project-agents
Create a project agent when you want a reusable worker for that project’s tasks. It combines a coding runtime, a model, instructions and allowed equipment. You need project edit access; the project must be active. Members see the project's agents on its **Agents** tab, which tells them to ask an Editor or Admin for a new one. Until a project has agents of its own, its tasks can go to the organization's [standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent). Only Owners and Admins can change secret grants.
## Prepare the first task
Choose a small outcome, such as reviewing a launch brief for missing approvals. The agent needs compatible [provider credentials](https://docs.tale.dev/platform/admin/providers) and an available [sandbox](https://docs.tale.dev/platform/admin/sandboxes). It can be configured without proving that a sandbox run will succeed.
Separate reusable instructions from the task. “Identify missing evidence and report the checks you performed” belongs on the agent. The document, review date and acceptance criteria belong on the task.

## Configure the agent
Open the project’s **Agents** tab and select **New agent**. Give it a recognizable **Name**, then choose **Agent type**, the coding [harness](https://docs.tale.dev/platform/agents/harnesses). Names are unique within the project; a project supports up to 50 agents.
You can also start from a task: while the project has no agent, **Create an agent…** under **Assignee** opens **New agent** over the task and assigns the agent you create to it.
Search **Model** by name or API ID. The same model can appear once per provider; read the provider on the entry before selecting it. Selecting an entry pins that pair for future runs. Subscription entries appear only for a compatible runtime, and a model whose tool calls need OpenAI’s Responses API, such as GPT-6.1 Sol, appears only for Codex.
An older configuration may name a model without a pinned provider. The dialog reports which provider currently resolves it, or why none can serve it. Select an entry if you want to pin that choice.
Under **Skills, connectors & tools**, add the bundles, services and platform operations the work needs. A new agent preselects the document skills `docx`, `pptx`, `xlsx` and `pdf` that are available to the project. They provide instructions for working with Word, PowerPoint, Excel and PDF files. Untick any the agent does not need; editing an existing agent keeps its saved equipment. Skill availability follows the project’s team access, not merely what you personally can see. A missing skill may therefore require a sharing change.

Read the **Writes data** label before granting a platform write tool: it authorizes real operations within that tool’s access rules. Connector broker actions available to agents are read-only; direct GitHub tooling or explicit secrets use separate access paths.
**Change task priority and agent assignment** lets a project agent prioritize existing tasks, assign them to an agent of the same project or leave them unassigned, without starting work. It is off until you grant it and is unavailable to automation agent nodes. The agent must read each task first and submit the current values with its changes; if someone changed those values meanwhile, the whole request is refused and the agent must read again. Ownership changes require an open task with no live run, pending review, or open question. A priority change alone preserves those handoffs. The tool never changes the status or reviewer, answers a human question, or starts a run. Only a live run with project-wide authority can use it; a run a Member started cannot.
**Review other agents’ task results** lets a designated reviewer decide a completed result from a different project agent. It is off until you grant it and is unavailable to automation agent nodes. Selecting an agent as reviewer does not enable this tool or start a run. The reviewer works from its own task, with current project-wide authority and the review permission still enabled. A decision records feedback and evidence; approval completes the reviewed task, while a request for changes returns it to **To do** without starting more work. Required human competences and workflow approvals remain protected. [Set up an independent reviewer](https://docs.tale.dev/platform/projects/task-automation#agent-review) explains the full flow.
**Start other agents on tasks** lets the agent put another agent of this project to work, for example a manager agent that hands out ready work and resumes an agent whose question it answered. It names a task, optionally the agent to assign it to, and a message the started run addresses first. The started run answers to whoever the manager's run answers to and names the manager as the agent that started it. An agent started this way cannot start further agents, a run a Member started cannot start any, and an agent already working another task, or a task an open task blocks, is not started. Grant this tool only to an agent whose instructions say which work it may hand out; [Task automation](https://docs.tale.dev/platform/projects/task-automation#work-an-automation-or-another-agent-starts) describes what such a run may do.
A run’s connector calls act for the member who started it, whether with **Start agent**, **Retry**, a move to **In progress** or an @mention of the agent. They use the organization’s [connector credentials](https://docs.tale.dev/platform/admin/connectors) and are recorded under that member. If that member leaves the organization or is disabled, the calls are refused: use **Cancel run** (or let the run finish), then start it again so it acts for you. When a comment restarts the run to guide it, as every runtime except Claude Code does, the calls act for the comment's author from then on.
Write **Instructions** that define responsibility, evidence and boundaries. For the launch reviewer: “Read the supplied brief. Report missing approvals and conflicting dates with the source passage. Do not mark the task complete.”
If the work needs **Secrets**, an Owner or Admin grants named organization credentials. The running agent can read their values, so use narrowly scoped, replaceable tokens. Shared names affect other agents and workflow nodes when their underlying value changes. A run a Member starts gets none of these secrets, nor the token of an equipped GitHub connection: an Editor or higher has to start work that needs them.
Select **Create agent**. Check the new row’s runtime, provider and model, then reopen it if you need to inspect the saved equipment or instructions.
## Assign and start work
Open a task in the same project, choose the agent as assignee and select **Start agent**. Assignment and execution are separate actions. Provide the files and acceptance criteria before starting. You don't need project edit access for this: a Member can put an agent to work on a task they created or that is assigned to them, and an Editor or higher on any task in the project. A run a Member starts keeps to that task, without the agent's secrets and in a workspace of its own; [Agent runs a Member starts](https://docs.tale.dev/platform/projects/tasks#agent-runs-a-member-starts) lists what changes.
The agent's report appears in task comments and collected files appear as deliverables. When an admin has turned on [image generation](https://docs.tale.dev/platform/admin/governance/content-models#let-agents-generate-images), the agent can also create images for the task; they appear among the deliverables and count against the member who started the run. Successful agent work moves to **In review** for its designated human or independent agent reviewer. Mention the agent in a comment to guide a running task or continue the conversation; the chosen harness determines whether guidance enters the existing process or starts a continuation.
[Task automation](https://docs.tale.dev/platform/projects/task-automation) explains progress, stopping and review. The ordinary Chat assistant remains separate, even when a chat has project context.
## The standard agent {#standard-agent}
A project without agents of its own can still take on agent work. Unless an admin has turned it off under [Governance > Models](https://docs.tale.dev/platform/admin/governance/content-models#standard-agent), **Assignee** offers **Standard agent** there, to everyone who can assign the task, Members included. The first time someone chooses it, Tale sets the agent up in the project and assigns it the task. You then start it like any other agent, or mention it in a comment. When it can't start for you, the comment box says so, and your comment is saved as a plain mention.

The **Agents** tab lists it with the **Standard** badge. Nobody edits it: its agent type, model and instructions follow the organization's settings, read again whenever a run starts, and its equipment is whichever of the document skills `docx`, `pptx`, `xlsx` and `pdf` the project can use when a run starts, so a skill an admin turns off drops out instead of stopping the agent. On **Automatic**, each run uses a model that the person who starts it may use, so it can run on different models for different people.
To give the project an agent of its own, select **New agent**. From then on, the project isn't offered the standard agent any more; the one already set up stays assignable until you delete it. Deleting it keeps the history of its tasks, and while the project has no agents, the next task given to the standard agent sets it up again.
## Update or remove an agent
Use the agent’s row actions to edit or delete it. The standard agent has only **Delete agent**. Changes apply to later runs; an active run keeps its starting configuration. Deleting the agent clears task assignment references while preserving task history. It also deletes the agent's [sandbox workspaces](https://docs.tale.dev/platform/admin/sandboxes#explain-why-a-workspace-disappeared) and their files, including each Member's. Review current work and preserve outputs you still need before removing the worker it belongs to.
If creation fails, use the displayed reason to distinguish a duplicate name, missing project access, an unavailable provider/model or a skill visibility issue. A run that fails for good says at the top of its task what went wrong and who can fix it; [When the agent can't finish](https://docs.tale.dev/platform/projects/task-automation#when-the-agent-cant-finish) lists the cases. Changing instructions does not fix those dependencies.
# Delegate a task to an agent
Source: https://docs.tale.dev/platform/projects/task-automation
A project agent works on a task and returns a result for review. Choose its assignment, start the work, and keep feedback on the task so the agent and reviewer have the same context. You need to be able to change the task: an Editor or higher can on every task in the project, and a Member on the tasks they created or that are assigned to them. The organization also needs a working provider, compatible harness, and available sandbox capacity.

## Prepare and start the task
1. Create a [task](https://docs.tale.dev/platform/projects/tasks) with the desired result, completion criteria, and input files.
2. Choose a [project agent](https://docs.tale.dev/platform/projects/project-agents) under **Assignee**.
3. If you can edit the project, choose a person or independent agent under **Reviewer**, or keep **Project default**. The initial project default routes reviews to an eligible task creator or project creator. [Choose an owner and a reviewer](https://docs.tale.dev/platform/projects/tasks#choose-an-owner-and-a-reviewer) explains the default, permissions, and pending handoffs.
4. Click **Start agent**, or move the task to **In progress**. When you assign the agent while creating the task, **Create and start agent** in **Create task** does this in the same step.
Assignment alone does not start execution. A task may remain assigned in **Backlog** while the team decides whether to proceed; until someone starts it, whoever can start the agent sees **Start agent** on the task with the note **The agent waits until you start it.** When started, the agent uses the task description, comments, and input files in its sandbox. Its run card shows whether it is queued or working. A run a Member starts keeps to its task and goes without the agent's secrets; [Agent runs a Member starts](https://docs.tale.dev/platform/projects/tasks#agent-runs-a-member-starts) lists what changes.
Agents are instructed to keep task updates, reports, related tasks and questions in the language of the task's title and description. If those do not establish a language, they use the organization's default agent language. An identifier, quarter or generated title template does not choose a language. Changing your interface language does not change the task's language; an explicit request to the agent can.
Workflow progress comments can carry translations for each supported interface language. The same saved comment then follows the reader's language selection. Comments without translations retain their original text.
## Read and accept the result
The agent posts its report as a task comment and adds produced files as deliverables. It then moves the task to **In review**. A human reviewer receives a notification and, when email delivery is configured, an email. An agent reviewer reads the pending review from its own task; routing a review to it does not start it automatically.
Tale lists delivered or skipped files in a separate system comment that follows your interface language. A missing or shortened report is noted there too, so the report keeps the task's language.
For a human review, read the report, open the deliverables, and compare them with the completion criteria. Move the task to **Done** only when you accept the work. Tale records the decision; the implementation agent cannot approve its own result.
For a review assigned to a person, **Reviewer** routes the notification and review queue without reserving the decision to that person. Other people who can change the task may accept it, subject to the organization's independent-review and competence rules. A pending review assigned to an agent must be explicitly transferred to an eligible person before a human can approve it. Changing the reviewer does not reassign the implementation work; [Choose an owner and a reviewer](https://docs.tale.dev/platform/projects/tasks#choose-an-owner-and-a-reviewer) has the details.
Changing **Reviewer** while the task waits in **In review** transfers the recorded request. The previous human reviewer's notification clears; a new human reviewer receives the request, while an agent-owned review waits for that agent. **Project default** uses the current project choice. A concurrent change to the review or result refuses the handoff, so read the refreshed state before trying again.
## Set up an independent reviewer {#agent-review}
1. Create a separate [project agent](https://docs.tale.dev/platform/projects/project-agents) for review. Under **Skills, connectors & tools**, grant **Find tasks**, **Read a task**, and **Review other agents’ task results**. The review grant is off initially and is not available to automation agent nodes.
2. Select that agent as the project's [default reviewer](https://docs.tale.dev/platform/projects/tasks#review-default), or explicitly under **Reviewer** on a task. For a review already waiting, use the task's reviewer control to transfer it; changing the project default alone does not.
3. Give the reviewer its own task and instructions to inspect only reviews assigned to it. Start that task directly or through an existing [schedule](https://docs.tale.dev/platform/automations/triggers#start-a-project-agent-on-a-schedule). Do not start a new implementation run on the task being judged.
4. Require the reviewer to read the current review, examine the actual report and deliverables, and record concrete check results and feedback. It must decide against the same completed run and evidence it read; a changed result requires another read and review.
For a captured agent review, `task_get.reviewFiles` lists attachments and outputs in pages of 50. Continue with `reviewFiles.page.continueCursor` as `reviewFileCursor`. The reviewer can use `task_review` with `operation: "stage_file"`, the current `expected: {approvalId, runId, evidenceRevision}`, and one listed `fileId` to copy an available file of at most 20 MiB into its workspace. It must then open and inspect the returned path: staging alone is not evidence that the contents were reviewed.
An `unavailableReason` explains why a file cannot be staged, including unsupported storage, missing or ambiguous metadata, a larger file, or a protected document binding. Document access rules still apply. A changed source or reviewer requires a fresh read. If authority changes during transfer, the operation refuses success; bytes already authorized for that workspace can remain there, but they do not authorize an outdated verdict.
Approval moves the reviewed task to **Done**. A request for changes records the feedback and moves it to **To do**, preserving its assignment. Neither verdict starts another run. Even an agent mention inside review feedback does not dispatch work: an authorized person, manager, or schedule must start the next attempt separately. The activity identifies the reviewer agent and its decision.
For one recorded request for changes, the manager calls `task_start_agent` with `taskId` and `resumeFrom: {kind: "review_repair", approvalId, runId}`. These IDs name the rejected review and the implementation it judged. The server selects that implementation agent and includes the reviewer’s saved feedback and `feedbackCommentId` in the repair brief. Any additional `feedback` must fit with that text and its identity prefix within the existing limit of 10,000 UTF-16 code units.
The repair starts only while the task is still **To do**, assigned to that same agent, with the same latest completed implementation and no newer review or intervening status, assignment or archive decision. Changing a value and then changing it back still invalidates the old intent. A `stale_repair` answer changes nothing: the manager reads the current task and retires the outdated intent. It never retries without the tagged guard. The repair uses the ordinary lifecycle and submits a new review when it finishes; `moveToInProgress: false` is refused.
If the response is lost, the same manager agent can repeat the guarded call from a currently authorized live run, including a later occurrence. `replayed: true` returns the original `runId` and `repairReceipt`, preserving the first issuing run and creating no further work. It reports a previous admission, not that the run is still active. Current delegation permission and project authority are checked again on every call. The existing single-live-run, busy-agent and three-automated-starts-per-task-per-hour limits still apply to new admissions. Repeated unchanged failures need a separate investigation task rather than an unbounded retry instruction.
To reconcile a lost verdict response across reviewer occurrences, read `task_get.reviewDecision`. It contains the latest validated native decision receipt, or `null` behind a newer pending, human or workflow review or when the receipt is unavailable. Compare its approval and run IDs with the recorded intent, and read the task’s current state separately. A historical receipt does not describe the current column, and `null` does not prove that a verdict failed to commit. An immediate identical `task_review` replay still requires the same live issuer.
The reviewer must be different from the agent that actually produced the result, for either verdict. It needs a live run with project-wide authority and the review grant still enabled; a run started by a Member cannot decide reviews. An independent-human-review policy or required human competence records routes new reviews to the human chain; a captured agent review requires explicit transfer to an eligible person. Workflow approvals and questions addressed to a person keep their own human gates. Read `pendingReview.agentReviewBlockedReason` for a current source, grant, identity or policy problem before attempting a verdict. If GitHub work is involved, the reviewer must check the referenced commit and checks itself: Tale records its evidence but does not independently verify GitHub's current state or merge a pull request as part of the verdict.
## Ask for changes
Add a task comment that names what needs to change and **@mention the assigned agent**. The mention is an instruction: an active agent can receive it during its run, and an idle agent starts a rework run that continues the previous conversation. The result returns to **In review**.
If you started a run, your mentions keep guiding it even after the task has passed to the agent, for example because your mention handed it a task that was assigned to you. When the agent's runtime restarts to take in a comment, which every runtime except Claude Code does, the rest of the run belongs to the comment's author: it counts against their limits, and its connector calls act for them.
A plain comment keeps a note without starting that agent action. The mention picker indicates when an agent cannot respond, for example because task automation is disabled or paused, or because you can comment on the task but not change it.
For an automation-owned task, mention the owning automation to request another run. Mentioning a different automation does not transfer ownership or start it. See [Automations](https://docs.tale.dev/platform/automations/concepts) for workflows that coordinate several steps.
A task can have only one queued, running, or waiting run at a time, whichever automation started it. Repeating a start request while one is active returns the existing run, even when it names another automation. Once it finishes, another start can create a new run and repeat the work. Check the current run and its effects before requesting another attempt.
## Let a manager agent keep the queue moving
A manager agent is a project agent whose instructions are to read the whole board, hand out ready work and answer the routine questions other agents leave, so that people see only what needs them. It reads with **Find tasks** and **Read a task**; neither tool changes anything.
### Read the whole queue in passes
**Find tasks** answers at most 50 tasks at a time. While a page says `isDone: false`, its `continueCursor`, passed back as `cursor` with the same arguments, returns the next page; the last page says `isDone: true`. A cursor that comes back with other filters, another order or from another project's run is refused, and so is a damaged one: it is never read as the first page. A total appears only when one page holds every matching task.
To walk a whole queue, use `order: "created"`. Tasks come oldest first and keep their place, so a pass lists each task at most once, as it stands when its page is read. The default order groups tasks by status and keeps each column's order within it, so a task that moves while the manager pages can be missed or listed twice.
Each listed task carries `pendingReview: null` or the captured `approvalId`, source `runId`, and typed `reviewer`. A reviewer finds its own queue with `status: "in_review"` and `reviewerAgentId` set to its agent ID; this matches the recorded review owner, not the implementation assignee or today’s project default. Keep that filter on every cursor request. The manager also scans all `in_review` tasks to find reviews whose agent has been deleted, then uses `task_get.pendingReview.agentReviewBlockedReason` to identify the recovery needed. Agent reviews stay in this task queue; a person’s Home review queue is for human requests.
A pass can outlast one run. Before its run ends, the manager saves a checkpoint comment on its own task: the pass, its order and filters, the next `continueCursor` and the last task it examined. Its next run finds that checkpoint with **Read a task** and continues from it. When a pass ends, or its cursor is refused, the next pass starts at the first page. A read that fails is not an empty queue: the manager reports it and stops.
### Tell running, finished and waiting work apart
**Read a task** names subtasks, blockers and comments by their IDs and pages back through older comments with `commentCursor`. It also lists the task's project-agent runs, newest first, each with the first 500 characters of the message its start carried, along with the task's automation run and any pending review:
- A run with `live: true` is queued or running, and nothing else starts on the task until it ends.
- A run that has settled, failed or been cancelled is finished; `settledAt` says when.
- `retryPending: true` means the newest failed run still has an armed automatic retry with budget remaining. Leave that retry to Tale. `false` means no retry is pending for that run; a missing field on an older platform means unknown. It supplies no provider reset time or permission to restart: first read the current task, assignee, runs and review, then honor any provider wait and admission `retryAfter`.
- A `workflowRun` waiting for an `ask` or an `approval` waits on a person. A `pendingReview` records its human or agent reviewer; a manager leaves the decision to that reviewer unless it is itself the assigned independent reviewer with the review grant.
The answer leaves out transcripts, error texts and results, and anything from another project.
### Answer a routine question
A routine question is one the manager can answer from what the project already records. Write the same protocol into the instructions of the working agents and of the manager:
1. The working agent posts the question as a task comment with a stable question key, its evidence and the question. It names the comment's ID and its own run ID in its result (**Read a task** on its task shows that run as the live one) and finishes the run instead of waiting inside it. Mentioning the manager in that comment starts nothing: a comment by an agent never starts an agent.
2. On its next pass, the manager answers only while the task still waits on that question: the run that asked is the task's newest run and has finished, nothing is live on the task, and the task waits in review of that run (`pendingReview.runId`). It keeps that run's ID and `pendingReview.approvalId` from this read. If someone has since moved the task on, by accepting or cancelling it or asking for changes, the question is no longer the manager's to answer, and it reports the question instead.
3. It posts its answer as a comment that names the question's comment ID; the answer stays on the task whatever happens next. It then resumes the agent with **Start other agents on tasks**, passing `resumeFrom: {runId, approvalId}` with the two IDs it kept, and a message that opens with the question's key, the run's ID and the answer's comment ID: a later **Read a task** shows only the first 500 characters of a start's message. The start checks, as it happens, that this is still the task's open question. Only then does it withdraw the pending review, without approving it, and resume the agent.
4. If the start answers `stale_question`, the question was overtaken between the manager's read and the start: a decision was made, a newer run or review exists, or the assignee changed. Nothing was changed. The manager reads the task again and drops the outdated question. It does not start again, and never without `resumeFrom`.
5. If the start's response is lost, the manager reads the task again before trying anything else: a newer run whose message opens with that question's key, run ID and answer comment ID means the start went through. If the same start is sent again, it answers `stale_question` and changes nothing.
A question from a standing task, one a schedule starts with `moveToInProgress: false`, waits for no review. The manager only posts its answer there, and the run at the schedule's next occurrence reads it. Neither the manager nor anyone else starts the task early to deliver the answer.
Answering an automation's question and deciding a workflow approval stay with people. A manager may decide a project-agent task review only through the independent-review flow above, when that review is assigned to it; reading or resuming a task does not approve it.
## Handle waiting and failed runs
| State or symptom | What to do |
| --- | --- |
| Waiting for a sandbox slot | Available capacity may be exhausted for the organization or shared infrastructure. Wait for a slot, or ask an admin to inspect [Sandboxes](https://docs.tale.dev/platform/admin/sandboxes). |
| Automatic retry is shown | Tale is retrying a recoverable failure. Read the attempt count and avoid starting another run. |
| **The agent couldn't finish this task** | No automatic retry follows. The notice says what went wrong and who can fix it, and **Details** beside the run shows what the run itself reported; [When the agent can't finish](https://docs.tale.dev/platform/projects/task-automation#when-the-agent-cant-finish) lists the cases. Resolve the cause, then use **Retry** to continue the conversation. |
| Reassignment is refused | Cancel the live run before choosing another assignee. |
| Agents or automations keep restarting one task | A task takes at most three starts of its agent by automations and other agents in any hour, ordinary automatic retries included; the cooldown exception below applies. The next start is refused, a retry past the limit is not started, and the timeline says **Run refused: agent runs are paused on this task**. Starts by people, and their retries, are never counted. Automation runs have no such cap: between two automations that keep mentioning each other, the one-engine rule is what stops a loop. Cancel the live run, then read the timeline before letting either start again. |
| **Run refused: agent is working on another task** | An automatic retry waited two hours for its agent, which is still working on another task. Nothing waits behind the refusal: start the task again once the agent is free, or leave it to the manager agent or automation that hands out the work. |
| The task cannot close | Finish its open subtasks first. |
Recoverable failures get up to three automatic retries, which start right away except in the cases below. A run that makes sustained progress for at least fifteen minutes receives a fresh retry allowance. This helps long work recover from interruptions; it does not prove the resulting work is correct.
An automatic retry continues the work of the person who started the run, so it starts only where that person could start the run now: the project must still be active, and they must still be allowed to change the task. If an admin archives the project, or that person leaves the organization or loses the right to change the task, no further retry starts and the run stays failed. The same applies to a mention that reaches the agent only after its run has ended. Once the project is restored, anyone who can change the task can use **Retry**.
An agent served by a subscription broker can lose its token while it works, when the broker refreshes the account. The retry then continues the conversation on a fresh token, and the attempt count does not advance: the retry shows the same count as the run it replaces, or **Resumed after a token refresh** when that run showed none or had worked for at least fifteen minutes, which earned it a fresh retry allowance. After two such interruptions in a row, a further one counts like any other failure.
A run can also fail to start because every account of its subscription broker is cooling down after a rate limit. Its retry is queued at once but starts only when the first account is available again, at most a minute later. The wait uses no attempt when the refused run immediately follows the same agent’s HTTP 429 failure. That single wait also adds no start to the task’s hourly automated-start count: the rate-limit failure already counted. A consecutive cooldown refusal, or one following any other failure, counts toward both limits.
### When the agent can't finish
When a run fails and nothing retries it, because its automatic retries are used up or a retry can't change the cause, the task says so at the top: **The agent couldn't finish this task**, with what went wrong and who can fix it. **Retry** there starts the agent again for whoever can change the task.
| The notice says | Who acts |
| --- | --- |
| A usage limit stopped the run | An Admin raises the limit; then start the agent again. |
| Something the agent needs is missing: its model, one of its skills, or the agent itself | An Editor or Admin fixes the agent on the project's **Agents** tab. |
| An attachment on this task is no longer in storage | Whoever can change the task removes the attachment or uploads it again; **Details** beside the run names the file. Then start the agent again. |
| The run reached its time limit | Start the agent again, or split the task into smaller ones. |
| The run waited too long for a free sandbox | Start the agent again when fewer agents are busy. |
| The AI model failed, or the run couldn't start or was interrupted | Start the agent again. If it fails again, show an Admin what the run reported under **Details**. |
Tale also tells the person who started the run and everyone watching the task: with the notification **Agent run failed** and, when the organization has a connected mailbox, by email. Only people who can still open the project are told. Starting a new run on the task marks the unread notification as read. Switch these notices off with **Agent escalations** under **Settings > Notifications**.
## Work an automation or another agent starts
A project agent can also be put to work without anyone pressing **Start agent**: by a [scheduled automation](https://docs.tale.dev/platform/automations/triggers#start-a-project-agent-on-a-schedule), or by another agent of the project that holds the **Start other agents on tasks** tool, such as a manager agent that hands out ready work and answers questions. The timeline lists such a run as **automation**, with a link to the automation run for Owners, Admins, and Developers, or as **delegated**, started by the agent that asked. The agent is told who started it, and a message that automation or agent passed reads as theirs, never as a person's review: where it contradicts the description or a person's comment, those win.
The run answers to whoever the requesting run answers to: the person who started it, or nobody for a schedule's chain, whose spend counts as automation spend. That person, or the schedule, has to be able to act in the project when the run starts; a lost Editor role, a paused schedule, or an automation removed from the project stops the next start. An agent another agent started cannot start further agents, and a run a Member started cannot start any. Such a start also checks what a person might forget: a task an open task blocks does not start, and an agent already working another task is not started twice. An automatic retry of such a run keeps to that rule: while its agent works on another task in the same workspace, the retry waits, checks again every five minutes and starts once the agent is free, without using an attempt. Each check reads the task afresh, so a move out of **In progress**, a reassignment, an archived task or project, or a lost role ends the wait. If the agent is still busy two hours after the failure, the retry does not start, and the timeline says **Run refused: agent is working on another task**.
A delegated run parks its result at **In review** for its recorded reviewer. Resuming a task that waits there withdraws its pending review without approving it. A start that leaves the card where it is (`moveToInProgress: false`) is refused under a card waiting for review, or a **Done** or **Cancelled** one, so earlier work is never presented for judgment while new work runs under it. A manager that answers an agent's question resumes it naming the run that asked and the review it waits at. The resumption happens only while that is still the task's open question. If a decision was made in the meantime (Done, Cancelled or another move), a newer run or review exists, or the assignee changed, the agent answers `stale_question` and nothing is assigned, withdrawn, moved or started. When your organization requires an independent human reviewer, the person the run answers to cannot accept its result.
## Cancel or pause work
Use **Cancel run** to stop the active agent. Anyone who can change the task can cancel its run, and so can the person who started the run, even after the task has passed to the agent. Moving a running agent-owned task out of **In progress** can also cancel the run; read the confirmation before proceeding. A task cannot have two active agent runs at once.
For a task an automation owns, the move stops the run and puts the task where you moved it in one step. If that move is refused, for example because you moved a parent task to **Done** while its subtasks are still open, the run keeps working and the task stays in **In progress**. **Cancel run** in the automation's panel on the task moves it to **Cancelled**, so the same open subtasks refuse it; to stop the run and keep the task open, move it to **To do** instead.
An admin can disable task automation for the organization. That blocks new starts while existing work finishes. Organization limits and budget policies still apply to each run; see [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits).
## Choose the right assignee
Assign a person when the task needs human judgment or work outside an agent’s permitted access. Assign a project agent for a bounded job using its configured files and tools. Use an automation when the work follows a defined process with stages, triggers, or connector approvals. A Member can choose only an automation built for tasks, one of those listed under **Automations** in **Assignee**.
For a first run, follow [Build your first agent](https://docs.tale.dev/tutorials/editor/first-agent-end-to-end). Keep the task small enough that you can inspect its result yourself.
# Manage tasks on a project board
Source: https://docs.tale.dev/platform/projects/tasks
A task keeps a piece of work together: its purpose, owner, status, files, and the conversation about the result. Use the project board for work a person will do as well as work you delegate to an agent. Every member who can open the project can create tasks in it; [who can change a task](https://docs.tale.dev/platform/projects/tasks#who-can-create-and-change-tasks) depends on your role and on whether the task is yours. The project must be active: an archived project is read-only until an administrator restores it.

## Create a task with a clear result
1. Open the project’s **Tasks** tab and click **Create task**.
2. Write a **Title** that names the result, such as “Review the launch brief”.
3. Use **Description** to explain what is needed and how the result will be checked. Add supporting files under **Attachments** when the work depends on them.
4. Choose **Status**, **Priority**, and an **Assignee** as needed. New tasks default to **To do**; use **Backlog** for a proposal the team has not committed to.
5. Click **Create task**. Open the new card to continue adding details. When an agent is the **Assignee**, **Create and start agent** creates the task and starts the agent in one step.
A title can have up to 200 characters and a description up to 20,000; most emoji count as 2. A longer description, pasted in or left on a task by an earlier import, is not cut: the field names the limit and counts the length, and **Create task** or **Save** stays unavailable until you shorten it.
Tale gives the task an identifier built from the project key, such as `WEB-1`. Use that identifier when referring to the work so similarly named tasks remain distinguishable.
A conversation can start a task too: **Create task** in the chat's header opens the same dialog with your request, the chat's files, and a link back to it, and the chat then follows the task above its message box. See [Turn a chat into a task](https://docs.tale.dev/platform/chat/basics#create-task-from-chat).
An automation built for tasks can also offer a template in **Create task**: its name then appears beside **Blank task** above the form. Choose it, enter the name the automation asks for, such as a quarter, and click **Create task**; the automation becomes the task's assignee. If a task already exists for that name, Tale opens it instead of creating a second one and says **A task for this subject already exists.** A Member can read and comment on it there, but change it only if it's theirs. Templates that set up folders or settings files in the project are offered to Editors and higher roles only.
A useful description states the input, the requested output, and a check for completion. For example: “Compare the review date in the attached brief with the meeting notes. Comment with any mismatch and cite both files.”

## Who can create and change tasks
Every member who can open a project can create tasks in it. Editors and higher roles can change every task in the project. On tasks they created or that are assigned to them, a Member can:
- Edit the title, description, attachments, subtasks, dates, priority, labels, and repeat.
- Assign the task to themselves, to another member of the project, to one of its agents, or to an automation built for tasks.
- Start, guide, or stop the agent, including with an @mention in a comment.
- Move the status, including accepting a result by moving the task to **Done**.
- Archive the task, or restore it.
The same goes for the subtasks under such a task, whoever added them, for example an agent that split up the work, so they never keep the Member from closing their task. Changing **Reviewer** requires project edit access, including on a task you created or own.
On other people's tasks, a Member reads and comments; mentioning an agent there leaves an ordinary mention that starts nothing. Everyone can edit and delete their own comments on any task they can read, and Owners and Admins can also delete other people's comments.
An archived task can be read but not changed. Until someone restores it, nobody can comment on it, edit or delete its comments, or change its dependencies; a task it blocks can still remove it under **Blocked by**. An agent run that was already working on the task still posts its result there.
Handing a task that was assigned to you to someone else, a person or an agent, also hands over the right to change it, unless you created the task. A run you started still answers to you, though: when your @mention hands the task to an agent, you can guide that run with further mentions and stop it with **Cancel run** until it ends.
A dependency belongs to the task it blocks, so a Member records dependencies for their own tasks only: under **Blocked by** on a task of theirs, or under **Blocks** on any task they can open, picking one of their tasks as the blocked one. The project's settings, agents, files, and label catalog stay with Editors and higher roles; a Member picks from the labels the project already has. Only Owners and Admins can delete a task; everyone else who can change it archives it instead.
Automations follow a narrower rule, because an automation acts as itself, with the organization's connector credentials, not as the person who starts it. A Member can hand a task only to an automation built for tasks, the ones listed under **Automations** in **Assignee**, or to the automation that already owns the task, and those are also the only ones they can start or ask for changes. Every other automation stays with Editors and higher roles.
### Agent runs a Member starts
A run started by someone who can't edit the project, such as a Member, keeps to its task:
- Its platform tools change only that task and the subtasks under it: the agent creates new tasks only as subtasks of that task, uses only labels the project already has, and can't sync items from other systems into the project.
- It can't save documents to the project. The files it produces still arrive on the task under **Deliverables**.
- The run gets neither the agent's **Secrets** nor the token of an equipped GitHub connection. The agent learns which credentials were held back and is asked to say so in its report when the work needs them; an Editor or higher then has to start it. Connectors equipped on the agent keep working and act for the person who started the run.
- It works in a workspace of its own, kept for that person's runs with this agent: files from runs that Editors started aren't there, and what this run leaves behind never reaches those runs. The same person's later runs with the agent find it again, until Tale deletes it: when the person leaves the organization or the agent is deleted, or once no run has used it for the number of days the organization sets under [**Days without use**](https://docs.tale.dev/platform/admin/sandboxes#delete-unused-workspaces-automatically).
The run can still read the project's tasks and knowledge, and it keeps these limits when an Editor guides it later. A run that an Editor or higher starts has the agent's full equipment, on any task. A Member's comment can change that: when the agent's runtime restarts to take in the comment, as [every runtime except Claude Code](https://docs.tale.dev/platform/agents/harnesses) does, the rest of the run counts as the Member's, with the same limits on its tools and credentials.
## Choose an owner and a reviewer
**Assignee** identifies who does the work: a person, a project agent, or an automation available to the project. **Reviewer** chooses a person, a project agent, or the **Project default** for checking the result. Changing that choice requires project edit access. A human reviewer also needs project edit access; an agent reviewer belongs to this project and must be different from the agent that produced the result.
Assigning an agent and starting its run are separate choices. After assigning it, click **Start agent**, or move the task to **In progress**. Read [Task automation](https://docs.tale.dev/platform/projects/task-automation) before starting work that can use connected services or produce files.
If the project has no agent of its own, **Assignee** offers **Standard agent**, the organization's agent for such projects. Choosing it sets the agent up in the project and assigns it the task; [The standard agent](https://docs.tale.dev/platform/projects/project-agents#standard-agent) explains how it works. Editors and higher roles can choose **Create an agent…** instead: **New agent** opens over the task, and the agent you create is assigned to it. When the standard agent can't run for them, for example because an admin has turned it off, Members are told to ask an Editor or Admin to add an agent on the project's **Agents** tab.

For a review assigned to a person, the named reviewer receives the request without being the only person allowed to accept it. Anyone else who can change the task can also accept the result: an Editor or higher, or the Member the task belongs to. When your organization requires an independent reviewer, the person who started the agent run under review cannot accept its result; when no agent run produced the result, the task's creator cannot. Required human competences still apply. A review assigned to an agent needs that agent's decision, or an explicit transfer to an eligible person before a human can approve it.
If the organization’s review policy cannot be read or is invalid, human approval is refused, including after a review is transferred from an agent to a person. An organization administrator must restore valid policy configuration before approval can succeed. Asking for changes or withdrawing a review keeps its existing behavior.
### Set a project review default {#review-default}
On the project's **General** tab, open **Task reviews** and choose **Default reviewer**, then save the project changes. The initial **Person** choice uses the task creator, then the project creator, if they can edit the project. Choose an independent project agent to route new reviews to it. This setting neither starts the agent nor grants its review permission; [Set up an independent reviewer](https://docs.tale.dev/platform/projects/task-automation#agent-review) covers those steps.
Tasks with **Project default** follow this choice when a new review starts. A task with an explicitly named person or agent keeps that choice. Reviews already waiting retain their recorded reviewer even if the project default changes. If someone changes the default while you are editing, discard the stale draft and choose again.
An agent default applies only when a native project-agent run produced the result and the organization does not require human independence or competence records. A person or automation submitting work without such a run uses the human review chain. Saving an agent choice requires its task review permission; a task cannot select its own implementation agent. A captured review never changes owner automatically after a grant, assignment or policy changes.
### Transfer a pending review {#transfer-review}
Open the task and read **Current review** below **Reviewer**: it names who owns the waiting review, which can differ from the current project default. Choosing another reviewer also transfers that pending review, without changing the assignee or starting a run. Choose **Project default** to hand it to the project's current default. If the reviewer or result changed since the task was read, the transfer is refused; check the refreshed review before choosing again.
An agent can review only a completed project-agent result from a different implementation agent. Assigning the task to someone else keeps the original result's producer but blocks the pending agent decision. To keep the new assignment and review that result, explicitly transfer the review to an eligible person; the organization's review policy still applies.
A live run or open question can prevent a transfer to an agent. For work without a supported completed agent run, or a review requiring human independence or competence records, choose an eligible person. If an agent is unavailable or lacks the review permission, the task shows that reason; it does not silently send the review to you.
The current review also explains self-review, changed implementation ownership, and unavailable policy. Restore the indicated condition or transfer it explicitly; settlement still records the result. A repeated task retains an explicit agent reviewer even if that agent was deleted or lost its grant, so repair that choice instead of silently inheriting a different reviewer.
## Use statuses to communicate progress
Change **Status** in the task details, or drag a card to another column on **Board**. The status picker is the keyboard-accessible alternative to dragging.
| Status | Meaning |
| --- | --- |
| **Backlog** | Proposed work that has not been committed to. |
| **To do** | Work ready to be picked up. |
| **In progress** | Work is underway. Moving an agent-owned task here starts its run. |
| **In review** | A result is waiting for its human or agent reviewer. |
| **Done** | The completed work has been accepted. |
| **Cancelled** | The work is no longer going ahead. |
For an agent-owned task, changing status can start or cancel execution. Read the action hint before moving it. An agent reports back at **In review**; it cannot mark its own work **Done**.
## Keep decisions with the work
Open the task to add a description, attachments, dates, labels, subtasks, or comments. Use comments for questions, decisions, and feedback that future reviewers need to understand.
Typing `@` in a comment opens the mention picker. A mention of an assigned agent is an instruction: it can steer a running agent or start another run when the agent is idle. A plain comment records the discussion without requesting that agent action.
Mentions in the task description work the same way when you save the task: the people you name are notified, and a named agent is steered or starts a run as described above. A run it starts moves the task to **In progress**, whichever column you created it in. When you edit the description later, only the mentions you add take effect. Rewording the text around an existing mention notifies no one again. The agent reads the description as it is when its run starts, so an edit you make while the run is still waiting is the version it works from.
Use **Subtasks** to split work that has separately checkable results. A subtask names its parent at the top of its details (**Part of …**); click it to go back up. While any of its subtasks is still open, a parent task cannot move to **Done** or **Cancelled**; every other status stays available, **To do** included. **Dependencies** shows which tasks block this task and which it blocks; circular dependencies are refused.
When you change a field, the task's **Activity** shows its value before and after. A field you clear shows what is left, such as **No due date** or **Unassigned**, rather than its old value alone. Titles, descriptions, labels, and file names appear exactly as they were written, even when the text is a status name such as `done`. Statuses, priorities, dates, and the words for an empty field appear in your language.
## Repeat a task
Give a task a repeat when the same work comes back on a schedule, such as a weekly status report. Each time the task closes, the next one appears in **To do**, due on the next day the repeat names.
Click **Repeat** below **Due date**, in the task's details or in **Create task**, and choose how the task repeats. A choice is saved as soon as you click it:
- **Never**
- **Daily**
- **Every weekday**, Monday to Friday
- **Weekly on …**, **Monthly on day …**, or **Yearly on …**, which take their day from the due date; a task without one uses its start date if that is still ahead, or else today

**Next due dates** shows when the next three tasks will be due. A task without a due date gets one when you choose a repeat: the first matching day from today, or from its start date when that is later. A monthly repeat on the 31st falls on the last day of shorter months. If you clear the due date of a task that already repeats, the repeat stays: the next task then comes when this one closes, due on the first matching day after that, and **Repeat** says so when you open it. Any change to the repeat gives the task a due date again.
If the dates exceed the supported calendar range, the preview explains this. Choose an earlier start or due date before setting the repeat.
### Set a custom repeat
For any other schedule, choose **Custom** in the same list. Pick **Day**, **Week**, **Month**, or **Year**, set how often the task comes back, and choose the weekdays, the day of the month, or the date, such as every 2 weeks on Tuesday and Thursday. **Next due dates** follows each change. Click **Save** to keep the repeat. **Cancel**, **Escape**, or a click outside the list discards your changes; the back arrow returns to the list and keeps them.
### Create the next task on the due date
Normally, the next task appears when you move this one to **Done** or **Cancelled**. When the work has to come back on time even if the last round isn't finished, open **Repeat** on a repeating task, select **Create the next task on the due date** below the dates, and click **Save**. The next task then appears at the start of the due date, midnight in the time zone of whoever set up the repeat, even while this task is still open. If the task is already due, the next one appears within a few minutes, and closing the task before its due date creates the next one at once. **System** appears as its creator in the task's activity. **Repeat** shows a calendar icon, and the tooltip of the repeat icon on **Board** and **List** ends in **next task on the due date**.
Open tasks no longer hold such a series back, so they can pile up when nobody closes them. A series never has more than 10 open tasks: the next one waits until someone closes one of them. Changing the repeat, or choosing **Never** and then a repeat again, still counts the tasks that remain open in the series.
### What the next task brings back
The next task has its own identifier and starts in **To do**. It keeps the title, description, priority, labels, attachments, assignee, reviewer, the people watching the task, and the repeat. Its subtasks come back with it, each in **To do** with its dates moved by the same step, along with the dependencies between them. Comments, dependencies on other tasks, archived subtasks, and the files an agent produced stay with the earlier task. People carry over only while they still have access: the assignee while they can still be assigned, the reviewer while they can still edit the project, and watchers while they can still see it. Whoever stopped watching the task doesn't watch the next one either, even if they created it.
The next task is due on the first day the repeat names after the earlier task's due date, and a start date keeps the same number of days before it. That due date is never in the past: close a task late, and the next one is due today or on the next matching day, so missed dates don't pile up as overdue tasks.
Under **Repeat**, the earlier task links to the next one, such as **Next task: WEB-13**; reopening the earlier task and closing it again creates no second one. On **Board** and **List**, a repeat icon marks the task that currently carries the series, and its tooltip names the repeat.
### Stop a series
When you close a repeating task, the message **Next task created** tells you when the next one is due and offers **Stop repeating**. The same button stays under **Repeat** on the task that created the next one, beside **Next task**, while the next task still repeats. If nobody has touched the next task yet (it is still in **To do**, unchanged, with no comments or agent runs), it is removed together with its subtasks. Otherwise it stays and no longer repeats. Either way, the series ends: on the task you stopped it from, **Repeat** reads **Never**, and its tooltip says **This series has stopped.** If you used the button under **Repeat**, the focus then moves to **Repeat**. You can also set **Repeat** to **Never** on the latest task of the series.
**Stop repeating** reaches only tasks you can change: the one you use it on, and the later tasks of the series that you can change too. Earlier tasks keep showing their repeat. If a later task belongs to someone else by now, for example after it was reassigned, it keeps its repeat and the series goes on from it; no next task is removed then.
Deleting the latest task of a series ends the series. The task before it doesn't create another one, even if you reopen it and close it again: it shows no repeat icon, and its **Repeat** stays locked, with the tooltip **Its next task was deleted. This task cannot repeat again.** Deleting an earlier task leaves the series going on from the latest one.
### When the repeat can't be changed
Point at **Repeat**, or move the keyboard focus to it, to read why it is locked:
- A task that already created its next task has handed the series on and doesn't repeat again, even if you reopen it. While the series goes on, change the repeat on the next task, which the **Next task** link opens. Once the series has stopped or its next task was deleted, **Repeat** says so.
- Any other task in **Done** or **Cancelled** keeps the repeat it closed with. Reopen it to change the repeat.
- A subtask has no repeat of its own. While its parent repeats, **Repeat** reads **With WEB-3**, for example, and each next task of the parent brings a fresh copy of the subtask. An archived subtask has no **Repeat**: it doesn't come back. Work that follows its own schedule needs a task of its own.
- A task an automation owns doesn't repeat, and assigning a repeating task to an automation ends its series.
- In **Create task**, **Repeat** reads **Never** while **Status** is **Done** or **Cancelled**, or while an automation is the assignee.
## Review the result before closing
For a human-owned task, compare the work with the description’s completion check. For agent work, read the report in the task’s comments and inspect any produced files. A finished run records that the agent has stopped working. Check the recorded review decision to see whether the result was accepted.
Move the task to **Done** when the result meets the requirement. If an agent needs to revise it, add specific feedback and mention that agent. [Task automation](https://docs.tale.dev/platform/projects/task-automation) explains retries, rework, and cancellation.
## Open your tasks from Home
[Home](https://docs.tale.dev/platform#home) lists the open tasks assigned to you and those waiting for your review, from every project you can read; **Tasks** above the list shows only them. A task you open there appears as a page of its own beside the Home panel, not in the board's dialog:
- The brief comes first as a card: the description, attachments, and subtasks.
- The discussion follows like a conversation, oldest first under day labels. It combines the comments with the task's history, such as status changes, assignments, and agent runs.
- The comment box sits at the bottom. Send with **⌘+Enter** or **Ctrl+Enter**, or with the round send button; **Enter** alone starts a new line. Type `@` to mention an agent or a person, with the same effect as in the board's dialog. Text you have not sent stays in the box for that task, here and in the board's dialog, and the task's row in Home shows **Draft** while you work elsewhere.
- **Details** beside the discussion holds the status, priority, assignee, reviewer, dates, repeat, labels, and dependencies, together with **Watch** and **Archive**. Organization owners and admins also find **Delete** there: it removes the task with its subtasks, their comments, and their files for good, and stops their running agent runs. **Hide details** at the end of the header folds it away, and **Show details** brings it back. In a window too narrow to keep both side by side, **Show details** opens it as a sheet over the discussion instead — from the side, or from the bottom on a phone.
**Board** in the header opens the project's task board. A task you open from the board still appears in its dialog; both views edit the same task. **Copy link**, the link icon beside **Board**, copies a link to this task page. To copy the task's identifier, such as `WEB-2`, click it in the line under the title; a message confirms each copy.
## Find work that needs attention
Use **Filter** to narrow the board, or switch to **List** to scan rows. Keep proposals in [Backlog](https://docs.tale.dev/platform/projects/backlog) until they are ready to start; use labels for distinctions that do not need another status.
**Search tasks** narrows the board together with **Filter**, and every task that matches both appears, however many there are. A search finds tasks whose title, description, or identifier, such as `WEB-12`, contains each word you type, and tasks with a comment that contains them all. The board holds up to 2,000 tasks. In a larger project, a note says that only the first 2,000 are shown; search to reach the others.
If the tasks can't be loaded, the board says so instead of showing empty columns, and your search and filters stay as they are. Choose **Try again** to load them. When a refresh fails, the tasks already on screen stay, with a note that they are shown as they were last loaded. A note also tells you when dependencies or agent and review activity can't be loaded, because blocked tasks, running agents, open questions, and pending reviews may then not be marked. **Try again** in a note reloads only what failed.
With the keyboard, press **Enter** on **Filter**: the panel opens on its first filter, **Assignee**. Press **Enter** to expand it and **Tab** to reach its options. The arrow keys choose one, and the board follows at once. **Space** chooses the option the focus is on, and clears it when it is already chosen. **Tab** moves on to the next filter, and **Escape** closes the panel.
In **Board** and **List**, press **Tab** until the task title is focused, then press **Enter** to open the task.
If you can edit the task, press **Space** on its title to pick it up, move it with the arrow keys, and press **Space** again to drop it. **Escape** cancels the move and leaves the task where it was. A screen reader names the task when you pick it up and announces its status and position as you move it.
If a change is refused, check the task’s current state before trying again: a live agent run blocks reassignment, open subtasks block closure, and your role and whether the task is yours determine whether you can change it at all.
# Skill library
Source: https://docs.tale.dev/platform/workspace/skills
A skill packages a repeatable way of working: writing release notes, checking a brief, or preparing a document in your house style. It contains a `SKILL.md` instruction file and, optionally, supporting files. Use **Settings > Skills** to maintain it once, then [equip the agents](https://docs.tale.dev/platform/agents/skills) that need it.
Skills take effect where an agent does the work: a [project agent](https://docs.tale.dev/platform/projects/project-agents) working a [task](https://docs.tale.dev/platform/projects/tasks), or an agent node in an [automation](https://docs.tale.dev/platform/automations/concepts). Chat answers questions and searches your knowledge; it does not use skills, run code, or produce files. To get a document made with a skill, assign a task to an agent equipped with it. Any member can create that task in a project they can open; equipping an agent with the skill needs the Editor role or higher.
Every member can create a skill. You can edit your own; editing or deleting another person's shared skill requires an organization administrator. Your organization can reserve sharing with everyone for some roles; see [Who can share with everyone](https://docs.tale.dev/platform/workspace/skills#who-can-share-with-everyone).
## Create a small skill
Open **Settings > Skills**, then **Add skill > Blank skill**. Enter a **Name** such as `brief-summary` and a **Description**:
```text
Summarize a project brief into its review date, owner, and open questions.
Use when someone asks for a handover or a quick check of a brief.
```
The name is a unique slug: lowercase letters, digits, and single hyphens, up to 64 characters. The description tells the model when the skill is relevant. Under **Visibility**, choose who sees the skill: **Organization**, or **Teams** with at least one team. Click **Create** to add it and open its editor.
Under **Instructions (body)**, give a short procedure and a recognizable result. For example:
```markdown
Read the supplied brief. Return a table with three rows: review date,
owner, and open questions. Quote the sentence supporting each answer.
Write "Not stated" where the brief supplies no answer. Do not infer a
launch date from a review date.
```
Add reference files only when they help perform the procedure. Keep detailed examples in those files and tell the agent when to open them.
**Organization** is preselected when you create a skill, unless your organization reserves it. If the content belongs to a different audience, change it under **Visibility**: **Teams** needs at least one team. Add an icon or labels if they will help people find the skill, then click **Save**.
Creating a skill does not equip an agent automatically. Open the intended project's agent and select the skill in its equipment. Run a small task with a known input and check the result against the instructions.

## Import an existing bundle
Use **Add skill > Upload zip** or **Upload folder**. The bundle must contain `SKILL.md` at its root. References, assets, and scripts can accompany it:
```text
brief-summary/
├── SKILL.md
└── references/
└── example-brief.md
```
The preview shows metadata, sharing, license, and the file list before **Upload bundle** writes anything. Check the contents and audience. Missing `visibility` means organization-wide sharing. If your organization reserves that and you may not publish, the preview says so and **Upload bundle** stays unavailable; add `visibility: team` and your team IDs to `SKILL.md`. When you create a team skill or change its teams, you may name only your organization's teams, and only your own unless you are an administrator. Keeping an existing team list is allowed even if a team has since been deleted. An `owner` in the file is ignored: a new skill is yours, and a replacement keeps its current owner or becomes yours if it had none. If the name already exists, Tale asks whether to replace that skill; replacement affects the agents that use it.
Importing a skill does not start a task or execute its files. Once equipped, however, its instructions guide a coding agent that may have tools, credentials, and a shell. Review unfamiliar instructions and scripts before equipping the bundle. A skill is not an additional permission boundary.
## Understand sharing
| Visibility | Who can read it | Which project agents can equip it |
| --- | --- | --- |
| **Organization** | Every organization member | Agents in any project |
| **Teams** | Members of the selected teams | Agents in projects with a matching team |
The project's access decides its equipment, even if you personally can read more skills. An organization-wide project can equip organization skills. Legacy private skills remain visible to their owner, but cannot be equipped; new private skills are not accepted.
Narrowing visibility asks for confirmation because some agents may lose access. Deleting a skill has the same practical consequence: runs that require the missing bundle cannot stage it. Check where a shared skill is used before restricting or retiring it.
### Who can share with everyone {#who-can-share-with-everyone}
By default, every member can share a skill with the whole organization. An administrator can reserve this for Editors and above, or for Owners and Admins, under [Skill sharing](https://docs.tale.dev/platform/admin/governance/policies-and-limits#skill-sharing), and can let individual members publish with the **Publish skills to the organization** competence. When your organization reserves it and you may not publish:
- **Organization** is unavailable under **Visibility**, and a new skill starts with **Teams**. You can share with your own teams.
- An organization-wide skill you created cannot be changed in place. Narrow it to your teams, with any other change in the same save, or delete it.
- The upload preview flags a bundle that would be shared with the whole organization, one without `visibility` included, and **Upload bundle** stays unavailable.
Skills that were already shared with the organization stay shared.
## See who created and changed a skill
The **Created by** column names the member who created each skill. Search the library for a name to find everything that person shared. The column shows **Built-in** for a skill without a recorded creator, such as the document skills your organization starts with, and **Configuration release** with the member whose upload installed it for a skill a managed configuration release installed. Once the creator leaves the organization, it shows **Former member**.

Open a skill to see **Created by** and **Last edited by**: the member whose save or upload in Tale produced the current version. **Last edited by** is left out when nobody has edited the skill since it was created, or when its file changed outside Tale since the last edit. The skill list in an [agent's equipment](https://docs.tale.dev/platform/agents/skills) names the creator under each skill as well.
Tale records creating, editing, uploading and deleting a skill, and every change to its visibility or teams, in the audit log. Administrators and owners find these entries under **Settings > Governance > Logs** in the **Skill** category.
## File reference
A minimal `SKILL.md` looks like this:
```markdown
---
name: brief-summary
description: Summarize a project brief. Use for brief handovers and checks.
visibility: org
---
Read the supplied brief. Report its review date, owner, and open questions.
Quote supporting text and mark missing information as "Not stated".
```
| Field | Meaning |
| --- | --- |
| `name` | Matches the bundle's folder name. `anthropic` and `claude` are reserved. |
| `description` | When and why the model should read the skill; maximum 1,024 characters. |
| `visibility` / `teams` | `org`, or `team` with the team IDs. The UI fills these in for you. |
| `owner` | The user ID of the member who created the skill. Tale sets it; a value in an uploaded file is ignored. |
| `license` | The terms supplied by the author. |
| `recommended-packages` | Suggested dependencies; importing does not install them. |
| `disable-model-invocation` | Requests explicit use of the skill. Treat this metadata as an instruction, not an access restriction. |
| `icon` / `labels` | Library presentation; up to eight labels of up to 40 characters each. |
Tale preserves unrecognized frontmatter keys. The frontmatter limit is 16 KB and the complete `SKILL.md` limit is 512 KB. Keep frequently read instructions much smaller than these ceilings.
## Update and troubleshoot
Open a row to edit its description, instructions, labels, and visibility. The **Bundle** tree lets you inspect supporting files. Changes are not pinned per agent: later staging uses the current bundle, so test shared changes with a representative task.
**Labels** takes up to eight comma-separated labels of up to 40 characters each. When the list is longer, or a label is too long, the field says what to remove or shorten and **Save** stays unavailable; nothing you typed is dropped. The **Bundle** tree remembers the folders you collapse, per skill, in your browser.
If Tale cannot load a skill or one of its files, the dialog says so and offers **Try again**. When a refresh fails, the version already on screen stays, unsaved changes included. A removed file is reported as no longer in the bundle, and an empty file as empty.
If an agent cannot find the skill, check that it is equipped and visible to the project. Tale shows the agent a description excerpt of up to 300 characters to help it choose a relevant skill, so start the description with when the skill applies. If it ignores an equipped skill, name the skill in the task and check the result against its instructions. Read [Skills on agents](https://docs.tale.dev/platform/agents/skills) for how the equipped bundle is staged and presented to the agent.
For an import error, check that `SKILL.md` is at the root, its frontmatter is valid, and its name is a valid slug. The error names rejected paths or size limits. To retire a bundle, open it and choose **Delete skill** after checking the affected agents.
# Set automation approval rules
Source: https://docs.tale.dev/self-hosted/configuration/approvals
Automation approval rules decide whether a live connector write runs immediately or waits for a person. The default requires approval for writes to external systems; writes through platform-authenticated internal connectors are allowed. Override that behavior for one organization when its review process needs a different boundary.
## Define the organization’s policy
Store rules in `TALE_CONFIG_DIR//governance/approval-policy.yml`. Each rule names exactly one `connector` or one qualified `action`, followed by `decision`.
This example requires review for writes through the internal `task` connector and allows `imap-smtp.send` without a separate approval:
```yaml
rules:
- connector: task
decision: require_approval
- action: imap-smtp.send
decision: auto_approve
```
`auto_approve` permits the matching write without a human review at this gate. Check the exact action, credentials and intended recipients before allowing an external action such as sending email.
Use connector and action identifiers from the shipped catalog, not translated display names. The action form is `.`; the allowed decisions are `auto_approve` and `require_approval`.
## Resolve overlapping rules
An action rule wins over a connector rule regardless of their order. Among equally specific matching rules, the last one wins. If no rule matches, Tale uses the internal-versus-external default above. Keep each target once where possible so readers can predict the result without tracing overrides.
The policy affects new gate evaluations. A pending approval is retained when you relax the rule; it does not silently become approved. This file also does not remove independent review gates such as publishing an automation or completing a task that needs review.
## Verify the effect
Test with an isolated automation and harmless data before enabling a rule for production work. Confirm one matching operation and one operation that should keep its default behavior. Inspect the pending approval and run trace through the [approval workflow](https://docs.tale.dev/platform/approvals/configure).
New write decisions read the current policy and organization slug without the short display cache. If the policy is invalid or its configuration root is unavailable, the operation stops before the write; repair the configuration before retrying. Only an absent policy file in an available configuration tree uses the default rules. A malformed `.yml` file never falls back to a sibling `.json`. Existing approval records keep their recorded decision, including pending approvals and previously granted operations.
# Choose an authentication setup
Source: https://docs.tale.dev/self-hosted/configuration/authentication
Tale supports local email-and-password accounts, organization-specific enterprise sign-in and identity supplied by a trusted reverse proxy. Choose based on where your team’s identities are managed and who owns account provisioning. Sign-in and provisioning are separate decisions: SSO authenticates a person, while invitations, sign-in provisioning or SCIM control membership.
## Choose the right integration
| Your environment | Configure | Main prerequisite |
| --- | --- | --- |
| Local accounts managed in Tale | Local sign-in and invitations | Stable deployment secrets and a reachable instance URL. |
| An existing corporate identity provider | Enterprise SSO: Microsoft Entra ID, generic OIDC, OAuth2 or SAML 2.0 | An IdP application configured with Tale’s exact callback or metadata URLs. |
| An application or proxy already authenticates its users | Trusted headers, per organization | A key from **Settings > Enterprise SSO** and a proxy that injects it with the identity headers. |
Enterprise SSO and trusted headers are both configured per organization. Plan and test changes to identity mapping before moving existing accounts to another mechanism.
## Establish the public URL first
Set `SITE_URL` and any supported base-path configuration to the URL people will actually open. Complete [TLS and domain setup](https://docs.tale.dev/self-hosted/configuration/tls-and-domains) before registering redirect URLs with an identity provider.
Keep `BETTER_AUTH_SECRET` stable across the backend processes that serve the instance. Use the generated secret from your deployment tooling or inject it from your secret manager. A mismatch can interrupt authentication even when the identity provider accepts the user.
## Use local accounts
Local sign-in stores password hashes in the application database. The [first-admin setup](https://docs.tale.dev/self-hosted/install/first-admin) creates the initial Owner; later members join by invitation. Configure mail delivery if your onboarding and password-recovery process relies on email.
Verify the complete flow with a test account: invitation, sign-in, sign-out and recovery. A working owner session does not prove that a new member can join.
Tale sends no verification mail, so an address is confirmed by whoever provisioned it: the setup wizard’s first Owner, an admin adding a person under **Settings > Members**, and the operator’s deployment all count as that assertion, and the account is usable straight away. Connected applications read this as the `email_verified` claim on the identity Tale issues, so a colleague an admin just added can sign in to them immediately. An account that comes from enterprise SSO, SCIM or trusted headers keeps whatever its directory reports instead.
## Connect enterprise sign-in
Configure the organization under **Settings > Enterprise SSO**. Microsoft Entra ID and generic OIDC use issuer discovery; OAuth2 takes explicit authorization, token and userinfo endpoints; SAML uses metadata, an assertion-consumer URL and signing certificates.

Use the callback and metadata URLs shown there rather than reconstructing them. Current native OIDC callbacks use `/api/sso/callback`; the compatibility route `/http_api/api/sso/callback` is also supported for existing registrations. The IdP registration must match the URL used by the flow.
Follow [Enterprise SSO and provisioning](https://docs.tale.dev/platform/admin/enterprise-sso) for protocol-specific setup, claim mapping, default roles, team synchronization and SCIM. Test sign-in in a separate browser session before ending the administrator session used to configure it. Discovery passing does not prove claims, group permissions or a complete sign-in.
## Trust an authentication proxy
An application that already signs its users in can hand them into one organization through its reverse proxy. An Admin turns the feature on under **Settings > Enterprise SSO**, in the **Trusted headers** card: choose the highest role the proxy may assert, then create a key and copy it, because it is shown once. Point the proxy's sign-in at `/api/trusted-headers/authenticate` with that key in the `Remote-Internal-Secret` header and the identity headers `Remote-Email`, `Remote-Name`, `Remote-Role` and `Remote-Teams`. The [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference) lists the `TRUSTED_*_HEADER` variables for renaming those headers.
The key decides the organization. A member of that organization signs in; an address the deployment has never seen becomes a new member with the asserted role; an existing account from another organization is refused. Owner is never assertable, and a role above the organization's ceiling is lowered to it. On every sign-in the member's seat follows the asserted role, so **Settings > Members** shows what the proxy asserted; an Owner seat never changes. Turning the card off refuses every key without revoking one; revoking a key does not end the sessions it started.
The proxy must remove client-supplied identity headers, set its own authenticated values and add the key only on the hand-off request. Anyone holding the key can sign in as any member the proxy names within that organization. Treat it like a password and rotate it from the card.
`Remote-Teams` lists team names separated by commas, for example `Finance,Operations`; an `id:name` entry such as `t-fin:Finance` is accepted too, and teams are matched and created by name. An omitted header leaves team management alone; a present empty header removes memberships previously granted by this synchronization. Invalid entries can therefore remove synchronized memberships. Manually granted memberships are preserved.
A proxied visitor is signed in on the app's first own request: when that request carries the key and the identity header but no session cookie, the backend mints the session right there and the app opens signed in — no sign-in page, no redirect. Should the app still land on its sign-in page (a stale cookie, for example), the page hands the browser to the hand-off address by itself, and a refusal comes back to that page with its reason. Routing the proxy's `/log-in` straight to the hand-off address remains supported; the address answers a success with a redirect to the app and a refusal as a page with its status. Because the proxy owns the session, the app offers no sign-out to a proxied member; after an inactivity sign-out the automatic sign-in pauses until the member continues from the notice.
To show Tale inside the application's own page rather than in a tab, an Admin lists that page's origin under **Embedding** on the same settings page. Tale's pages then answer with a `frame-ancestors` policy naming `'self'` and the listed origins instead of refusing every frame, and the `X-Frame-Options` header is left off. The list belongs to one organization, but the sign-in shell is one document for the whole deployment, so an origin any organization admits may load it. The browser sends the session cookie into a frame only when the surrounding page is on the same site as Tale, for example a subdomain of the host or Tale served under the host's own domain; a cross-site frame shows the sign-in page instead.
## Diagnose sign-in failures
| Symptom | Check first |
| --- | --- |
| IdP rejects a redirect | Compare its registered URL with the URL shown by Tale, including scheme, host and path. |
| Redirect returns but sign-in fails | Check callback reachability, cookies and the configured claim names. |
| Member receives the wrong role | Check default role and mapping rules with that person’s actual claims. |
| Synchronized teams disappear | Inspect the groups claim or `Remote-Teams` value; distinguish absent from empty. |
| Trusted-header sign-in is refused | Check that the card is on for that organization, that the key is not revoked, and the proxy's header names. |
Use a staging organization for mapping changes and keep a tested administrative recovery path. Authentication changes can affect every member whose identity depends on the connection.
# Release client configurations
Source: https://docs.tale.dev/self-hosted/configuration/config-releases
A configuration release installs a reviewed workflow and its owned skills into an existing organization and project. Its identity is the full source commit. Use this procedure when the application runtime stays in place; use [managed deployments](https://docs.tale.dev/self-hosted/install/cli-install#managed-deployments) for a new instance or runtime change.
## Know what each command produces
| Command | Result to check before continuing |
| --- | --- |
| `config build` | A manifest and compiled archives from a committed source revision. |
| `config verify --rebuild` | Independent reconstruction agrees with the reviewed artifacts. |
| `config stage` | A transferable directory containing only the deployment files and their hashed inventory. |
| `config deploy` | The workflow and owned skills are installed, read back, and recorded in a persistent receipt. |
| `config verify-native` | A read-only comparison with the currently installed content. |
A **native** ID or session in these commands belongs to the target Tale instance. A **receipt** records a deployment result; it does not replace checking the current server. Format and byte verification do not prove the automation's business result, so keep domain tests in the client repository.
## Before you begin
Install the [Tale CLI](https://docs.tale.dev/self-hosted/install/cli-install) and pin a revision that supports the target server's pack format and APIs. Configuration commands take explicit source and target options; they need no local `tale.json`, Docker context or adjacent Tale source checkout. The bundled parser and validator check supported fields, without adding newer server capabilities to an older instance.
You need a committed client descriptor and pack, an existing organization and project, and an authorized native operator session. If the pack owns skills, use that user's native ID as the build owner. The ID is distinct from a project, organization or external identity ID.
For a new instance without native IDs, use managed deployment with an explicit fresh identity, symbolic project and `skillOwner: "operator"`. It carries verified source and compiles only after proving the native operator; these standalone release commands still require resolved IDs. Keep business configuration in its client-owned source tree. External model settings belong to the deployment declaration.
The examples use the synthetic `example-team` client and `document-review` automation. Inject the full operator session cookie through `TALE_CONFIG_COOKIE` from your secret manager. Keep it out of arguments, source, archives, receipts and logs.
## Commit the descriptor and pack
Store the descriptor at `tale/client.json`, packs beneath `tale/packs/`, and domain tests beside them. Keep any retained historical release catalogue there too. Descriptor paths resolve relative to its own directory.
Ignore `.tale/` if it is not already in the client's `.gitignore`. Default builds keep local coordination state there; explicit output operations coordinate beside their output. Maintained configuration uses `tale/` without the dot.
```json
{
"schemaVersion": 1,
"clientId": "example-team",
"sourceRepository": "https://github.com/example-team/client-app",
"automations": [
{
"name": "document-review",
"displayName": "Document review",
"packPath": "packs/document-review",
"releasesPath": "releases/document-review",
"logicalSkillSlugs": ["record-check"],
"requiredExternalSkills": []
}
]
}
```
`logicalSkillSlugs` lists the skill directories carried by this pack. `requiredExternalSkills` lists dependencies already installed in Tale: the CLI checks their presence, but this release does not pin their bytes. Keep credentials, target hostnames and project IDs in deployment configuration. Commit the descriptor and pack; the compiler reads Git objects rather than uncommitted edits.
## Build and verify the source commit
Set `CONFIG_REPO` to the checkout, `CONFIG_SOURCE_COMMIT` to its full 40-character source commit, and `TALE_NATIVE_USER_ID` to the native operator ID. Choose a new absolute `CONFIG_BUILD` output directory outside the checkout. These commands build the release and independently reconstruct its bytes.
```bash
tale --json config build \
--repo "$CONFIG_REPO" \
--descriptor tale/client.json \
--automation document-review \
--source-commit "$CONFIG_SOURCE_COMMIT" \
--skill-owner "$TALE_NATIVE_USER_ID" \
--output "$CONFIG_BUILD"
tale --json config verify \
--repo "$CONFIG_REPO" \
--descriptor tale/client.json \
--automation document-review \
--manifest "$CONFIG_BUILD/$CONFIG_SOURCE_COMMIT.json" \
--rebuild
```
The default manifest uses schema 4/compiler 3 and records `releaseRef` equal to `sourceCommit`, plus the pack tree, descriptor hash and complete compiled inventory. The output contains a canonical ZIP, one ZIP per owned skill and a workflow-only installation ZIP. Owned skill slugs carry the full source SHA, with compiler metadata preserving their logical identity. The complete skill bytes include the declared owner; changing that owner requires a new source commit and release.
Require identical bytes on rebuild and run the client's correctness tests against the extracted canonical ZIP. Preserve the reviewed artifacts and record `artifactSha256` as `CONFIG_ARTIFACT_SHA256`. Native format validation proves that the pack can be interpreted, not that its business results are correct. New source-based deployments do not need an extra commit containing generated release files.
## Prepare the transfer directory
Use a checkout whose `HEAD` matches `CONFIG_SOURCE_COMMIT`. Set `CONFIG_STAGE` to a new absolute output directory outside that checkout. The optional `DEPLOYMENT_COMMIT` records the full commit of your deployment declaration; omit `--deployment-ref` if you do not track one.
```bash
tale --json config stage \
--repo "$CONFIG_REPO" \
--descriptor tale/client.json \
--automation document-review \
--config-ref "$CONFIG_SOURCE_COMMIT" \
--skill-owner "$TALE_NATIVE_USER_ID" \
--client example-team \
--deployment-ref "$DEPLOYMENT_COMMIT" \
--output "$CONFIG_STAGE"
```
The stage builds the committed descriptor and pack, independently verifies the archives and prepares only allowlisted deployment files with a hashed inventory. Dirty source edits do not enter it. Check that its artifact hash matches the reviewed build. Preserve the stage as one unit during transfer; the native destination needs neither the client checkout nor its Git credential.
Pin the client repository URL and full source SHA, Tale CLI revision and optional deployment revision. Source provenance also depends on your trusted checkout: a URL in a descriptor does not establish which remote supplied a local Git object.
## Deploy and read the result back
Set `TALE_CONFIG_URL`, `TALE_CONFIG_ORIGIN`, `TALE_ORG_ID` and `TALE_PROJECT_ID` to the approved native target. Keep `CONFIG_RECEIPT` on persistent storage. After reviewing the stage, supply `--yes` for an authorized unattended deployment, then run a separate read-only verification. Use the same optional deployment reference as the stage.
```bash
tale --json --yes config deploy \
--stage "$CONFIG_STAGE" \
--url "$TALE_CONFIG_URL" \
--origin "$TALE_CONFIG_ORIGIN" \
--org "$TALE_ORG_ID" \
--project "$TALE_PROJECT_ID" \
--receipt "$CONFIG_RECEIPT" \
--config-ref "$CONFIG_SOURCE_COMMIT" \
--source-repository https://github.com/example-team/client-app \
--artifact-sha256 "$CONFIG_ARTIFACT_SHA256" \
--deployment-ref "$DEPLOYMENT_COMMIT"
tale --json config verify-native \
--stage "$CONFIG_STAGE" \
--url "$TALE_CONFIG_URL" \
--origin "$TALE_CONFIG_ORIGIN" \
--org "$TALE_ORG_ID" \
--project "$TALE_PROJECT_ID" \
--config-ref "$CONFIG_SOURCE_COMMIT" \
--source-repository https://github.com/example-team/client-app \
--artifact-sha256 "$CONFIG_ARTIFACT_SHA256" \
--deployment-ref "$DEPLOYMENT_COMMIT"
```
The target URL is where the API is reachable; the origin is the canonical browser origin, including when the API URL is loopback behind a proxy. The authenticated native user must match the declared skill owner.
Deployment creates missing owned skills through native create-only upload and verifies every installed byte. Existing exact bytes can be reused; differing bytes at the release slug cause a refusal. Workflow-only import cannot write skills. Before reporting success, the CLI checks the deployed workflow, settings, presentation, task contract and project binding.
Run `verify-native` after deployment and operational tests. It imports nothing and creates no version or receipt. A repeat deployment also reads current native content before reporting an unchanged release; a stored receipt alone does not prove current bytes.
## Recover a stopped deployment
Keep the exact stage and receipt while investigating. Choose the next action from the state the CLI reports:
| State | Safe next step |
| --- | --- |
| A trusted receipt records partial progress | Retry with the same stage, target, and receipt. Exact skills can be reused. |
| A matching version is already deployed | Read it back with `verify-native`; a repeated deploy also verifies before reporting no change. |
| An upload response was lost and only an unpublished version is visible | Stop and investigate. The native API cannot read its complete task contract before deployment, so the CLI cannot prove that version safe to reuse. |
| A release skill slug exists with different bytes | Preserve the evidence and identify the conflicting release or edit. Do not overwrite it to make verification pass. |
| A receipt is unreadable or names another target | Restore the correct record or resolve the mismatch before retrying. Never manufacture a successful receipt. |
Coordinate deployments that share a destination. Local locking does not prevent another host or an administrator from changing native content. After recovery, rerun the client's operational checks and the independent native verification.
## Reconstruct a historical release
Existing semantic releases remain a compatibility path: `build --config-version` selects schema 3/compiler 2; `stage --config-version` uses the committed catalogue and its explicit catalogue/ops pins. Keep original manifests and archives unchanged. A descriptor can register checksummed historical source snapshots for offline reconstruction; only that format's supported fields can be verified. Legacy shared skills may be reused only when explicitly allowed and already byte-identical. Restore different historical content through a new release.
Historical reconstruction needs additional tools. Schema 1 uses Git's `archive --mtime` capability; check your selected Git supports it. Schema 2/compiler 1 uses Python 3's standard library. Schema 3/compiler 2, schema 4/compiler 3 and native deployment do not need Python.
Retain the source, client tests, reviewed archives, CLI revision, deployment pins, and receipt together. Keep session cookies and credentials in your secret manager, outside those artifacts. [Upgrades](https://docs.tale.dev/self-hosted/operate/upgrades) and [Backups and restore](https://docs.tale.dev/self-hosted/operate/backups-and-restore) cover recovery of the surrounding runtime and data.
# Choose and move data stores
Source: https://docs.tale.dev/self-hosted/configuration/data-residency
Choose storage for three kinds of data: application records, searchable knowledge, and original files. Moving one does not move the others. Storage placement also does not determine where a model provider or connector processes a request; include those destinations in your residency assessment.
## Choose the scope of the change
| Store | Deployment-wide setting | Organization-specific setting |
| --- | --- | --- |
| Application database: users, chats, runs, and audit data | `DATABASE_URL` | No separate application database setting on this page. |
| Knowledge database: extracted text, embeddings, search indexes, and crawled content | `KNOWLEDGE_DATABASE_URL` | **Settings > Data residency > Knowledge database** |
| Original files: documents, attachments, audio, and generated media | `OBJECT_STORE_*` | **Settings > Data residency > Object storage** |
The packaged stack puts `tale_app` and `tale_knowledge` in one Postgres service, while keeping them separate databases. Other layouts can use separate services. The environment values generated by the deployment select the defaults; a bare application process does not invent working object-store credentials.
Organization settings require Admin or Owner permissions. Without an organization-specific connection, Tale uses the deployment default and keeps organization data scoped. An invalid configured knowledge connection fails rather than silently using a different database.
## Prepare an external database
Provision the database and credentials before changing Tale. The application database needs a role with permission to apply its schema migrations. The knowledge database needs `vector` installed; `pg_search` enables the BM25 part of hybrid search. A target with only pgvector provides vector search without that keyword leg. Tale prepares knowledge schemas and tables but does not install extensions on a database you own.
Use a direct or session-compatible Postgres connection. Transaction-mode pooling is incompatible with session locks, `LISTEN`, and prepared statements used by the stack. For `sslmode=verify-ca` or `verify-full`, supply the required PEM roots through `POSTGRES_CA_FILE` and mount the file in the backend roles. Check connectivity and permissions from the deployment network.
Before a cutover, choose how existing application or knowledge data will reach the destination, and take coordinated backups. Saving a database connection only changes where subsequent operations go; it does not copy old rows or reindex old documents automatically.
## Change deployment defaults
Update `.env`, then deploy or recreate the affected backend services. `docker compose restart` keeps the old environment. If you are moving both databases out of the bundled service, preserve its old volume until the new stores and recovery plan are accepted.
For the default object store, the backend reconciles `default/object-storage/connection.json` and its secret sidecar from the environment at startup. The boot result distinguishes `seeded`, `reconciled`, `skipped` when credentials are absent, and `ignored` for an operator-managed file. `"managedBy": "operator"` makes you responsible for that file instead of environment reconciliation.
Changing the default bucket or endpoint does not copy existing blobs. Copy them with storage tooling, retaining keys and required metadata, and coordinate the switch before removing the old store. An external database or bucket is outside the CLI volume snapshot's data coverage; update [Backups and restore](https://docs.tale.dev/self-hosted/operate/backups-and-restore) procedures as part of the change.
## Connect an organization's knowledge database
1. Open **Settings > Data residency** in the target organization and configure **Knowledge database** with host, port, database, user, SSL mode, and password.
2. Use **Test connection**. It first checks the fields the way **Save** does and names a missing host, database, or user under its field instead of running. Check both connectivity and extension availability; a successful connection does not prove the old corpus has migrated.
3. Save only when the destination and existing-data plan are ready. Subsequent requests use the selected connection without a container restart.
4. Index a controlled document, search for a known phrase, and verify that pre-existing content required by your users remains available.
The files live under `$TALE_CONFIG_DIR//knowledge/`: `connection.json`, `connection.secrets.json`, and `embedding.json`. Secrets use SOPS when an age key is configured. Removing the connection returns routing to the deployment default; it leaves data in the external database and makes that data inaccessible through the removed connection.
### Match the embedding model to the corpus {#the-organizations-embedding-model}
In **Embedding model**, choose the provider and stored credential, then the model. **Model** lists the embedding models the provider's catalog carries and fills the vector width the catalog states. A provider that offers no embedding model at all, such as Anthropic, cannot be chosen: the row reads **Cannot embed**, and you pick another provider. Where Tale knows no vector width for the provider, type the model tag (for Azure OpenAI, your deployment name) and the vector width that model produces. That applies to a shipped provider whose catalog lists no embedding model, to a provider your organization defined itself whose model listing carries none, and to Azure OpenAI, whose deployments carry your own names. The provider's [`embedding` declaration](https://docs.tale.dev/self-hosted/configuration/providers#what-a-connector-declares) decides between the two, and saving refuses a provider declared unable to embed however the request arrives. If Tale cannot check the declarations, the section says so and offers **Try again**; until the check answers, no model can be chosen. An optional base URL selects an OpenAI-compatible endpoint. Without a configured embedding model, knowledge indexing and search cannot operate normally.
Vector width is pinned per database on first use. Organizations sharing a database must use that width; a different width needs a separate compatible database. Changing the model can also make existing vectors incompatible even when the width stays the same. Plan reindexing with the chosen model instead of mixing embeddings blindly.
`embedding.json` can set `minSimilarity`, the assistant search's vector-leg floor; its default is `0.45`. The settings form preserves this file value but does not expose a field for it. Tune it against representative queries. REST knowledge search applies a floor only when its request supplies one; this is not a universal score threshold for all searches.
### Pace requests to a self-hosted embedding server {#embedding-server-capacity}
Two more optional `embedding.json` settings describe how much work the embedding server can take. Set them when you run the server yourself, for example a model server on your own hardware that computes one request at a time and queues the rest. Like `minSimilarity`, they exist only in the file: the settings form keeps them when you save, and the CLI declares them in the `knowledge-embedding` resource.
- `maxConcurrentRequests` (1 to 64, default 3) is how many embedding requests to this model Tale keeps in flight at once for the organization. Document indexing, the indexing of incoming email, website scans and searches share this limit. Further requests wait in arrival order, except that a search query goes ahead of waiting indexing batches. Each Tale process counts separately, so the API and every worker replica can each reach the limit. A lower value takes effect at once; a higher one once the requests started under the old value have finished. On a server that computes one request at a time, a higher value adds no load; each request only waits longer.
- `minTokensPerSecond` (any positive number) is the slowest rate at which the server computes embeddings for this model under its usual load. Measure it while other work runs on the same hardware, such as a chat model, but leave out the time a request waits behind other requests. Tale adds that waiting time itself.
- `maxTokensPerMinute` and `maxRequestsPerMinute` (whole numbers) pace what Tale sends this model within any one minute, for the organization in each Tale process. Set them to a hosted provider's per-minute limits, which are counted per provider account across every deployment that shares the key: an indexing batch that would cross a limit waits until enough of the last minute has aged out, so a crawl of hundreds of pages is paced instead of refused with `429`. Tale estimates tokens from characters, on the high side, keeps a sixtieth of each limit within any one second as well — a provider enforces its per-minute figure per second too — and sizes each request to fit that second. A `429` the provider still answers pauses every request of the organization to that model and is retried on a minute scale; a search query is not held back by either. Alibaba Cloud Model Studio, for example, allows text-embedding-v4 1,800 requests and 1,000,000 tokens per minute.
```json
{
"providerSlug": "local-embedding",
"model": "example-embedding",
"dimensions": 1024,
"baseUrl": "https://embeddings.example.internal/v1",
"maxConcurrentRequests": 2,
"minTokensPerSecond": 800,
"maxTokensPerMinute": 1000000,
"maxRequestsPerMinute": 1800
}
```
Each embedding request has a ceiling: 15 minutes for indexing, and 5 minutes for a search query, which a chat answer waits on. Without `minTokensPerSecond`, Tale cannot tell how long the server's queue may take, so a request may use its whole ceiling. With it, Tale gives each request time for the work that can be ahead of it or beside it on the server, plus its own. That work is the request's own tokens, estimated generously from its characters, plus the other requests this Tale process may have in flight and `maxConcurrentRequests` more from other clients. Tale counts each of those requests as at least a full batch of 64 texts with 1,024 tokens each, divides the total by `minTokensPerSecond`, and adds 50%, but never allows less than 60 seconds or more than the ceiling. With the example above, a full batch of ordinary text gets about eight minutes, and a search query gets its five-minute ceiling. A search query also ends after five minutes in total, including any wait for a free slot. If more clients share the server than one more Tale process with the same limit, state a lower rate.
A request that runs out of time is not sent again at once: the server had it the whole time, and a repeat would only lengthen its queue. A refused connection, a rate limit or a server error is retried after a pause that grows with each attempt, or after the pause a busy server asks for with `Retry-After`, up to one minute. A server that asks for a longer pause is left alone. When one batch of a request made of several batches fails, for example on a long web page, Tale cancels the other batches, whether they are running or still waiting, so the server stops working on them.
Indexing a document gets at most 15 minutes per attempt. When a large document or a long queue needs more, the attempt stops at that limit and cancels its open request; a request that runs out of time ends the attempt too. The next attempt starts after a pause that grows with each attempt and continues after the chunks already stored. If a document is still unfinished after six attempts, it shows as failed; **Retry indexing** continues from the stored chunks.
## Connect an organization's bucket
1. Provision an S3-compatible bucket and the required object permissions. Configure CORS for the actual browser origins and the needed `GET`, `PUT`, and `HEAD` methods.
2. In **Object storage**, enter region, endpoint when needed, bucket, optional key prefix, and credentials. Use path-style addressing when your store requires it.
3. Run **Test connection**, then save. A missing region or bucket, or a value past a field's length limit, is named under its field before anything is sent. The server test writes, reads, and deletes a test object; it does not test browser CORS.
4. Upload and download a controlled file in the browser before relying on the new connection.
New uploads use the organization's bucket. Earlier default-store files can remain readable through mixed references, so connecting the bucket does not by itself meet a requirement to relocate history. Configuration lives under `$TALE_CONFIG_DIR//object-storage/connection.json` and `connection.secrets.json`.
Removing this connection sends new uploads to the default store. Existing objects remain in the organization's bucket but require the connection to be restored before Tale can read them again.
### Move existing files deliberately
With the organization bucket saved, use **Move existing files** in the same section. Review any preview, confirm the move, and follow its progress. Keep the connection stable until it finishes.
The backfill walks that organization's referenced documents and history, uploaded files, synthesized audio, and video transcripts. It copies each blob with its content type, checks the destination size, then deletes the source copy. It preserves object keys and can resume a previously verified copy without duplicating the move. This is a move, not an additional backup or a cryptographic content audit.
The destination must differ from the default store. If the run fails, inspect its last error and progress before starting another; preserve both stores until the final result and representative old-file downloads are verified. See [Secrets with SOPS](https://docs.tale.dev/self-hosted/configuration/secrets-with-sops) for protecting the connection sidecars.
# Environment reference
Source: https://docs.tale.dev/self-hosted/configuration/environment-reference
Use this reference to find a deployment variable, its default and the process that needs it. The project `.env` is one way to supply values; container environment settings and secret-manager injection are also deployment inputs. The [example environment file](https://github.com/tale-project/tale/blob/main/.env.example) carries the corresponding source configuration.
After changing an environment value, recreate the consuming services through your deployment workflow. `docker compose restart` retains the container’s existing environment. File-based organization configuration has a separate lifecycle.
## How to read this page
Tables list names, defaults, and purpose. Required values must reach the consuming service; deployment tooling may generate some of them. Optional values can remain unset. A documented default can come from the shipped Compose configuration rather than the process itself.
Use the example file alongside this reference and inspect your effective service environment before diagnosing a missing value.
## Domain identity (required at first boot)
| Name | Default | Description |
| ----------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `HOST` | `localhost` | **Required.** Hostname without protocol. Used for Docker networking and outbound email. |
| `SITE_URL` | `https://localhost` | **Required.** Full canonical URL including scheme and any non-standard port. Auth callbacks and external links use this. |
| `ADDITIONAL_SITE_URLS` | unset | **Optional.** Other origins the same deployment answers on, comma- or whitespace-separated (e.g. `https://a.example,https://b.example`). Each is a full entry point. See [TLS and domains](https://docs.tale.dev/self-hosted/configuration/tls-and-domains#several-domains-at-once). |
| `TOTP_ENVIRONMENT` | unset | Authenticator environment label for `backend-api` and the `all` role: 1–32 letters, digits, underscores or hyphens, trimmed and uppercased. `pr` or unset keeps `Tale`; `te` names newly generated entries `Tale `. Invalid labels prevent backend startup. |
| `BASE_PATH` | unset | **Optional.** Path prefix for subpath deployments behind a reverse proxy (e.g. `/app`). Leave unset for root deployments. |
| `DOCS_URL` | `https://docs.` | Public origin for the proxy’s separate documentation host. The deployment must also include the docs service. |
`SITE_URL` identifies the canonical public origin. Keep scheme, hostname, and port consistent with the browser address and registered callbacks; `BASE_PATH` supplies a deployment path prefix. A trailing slash is normalized by the proxy. Additional addresses belong in `ADDITIONAL_SITE_URLS` as bare origins. Invalid additional origins stop backend startup.
The prose documentation uses its own origin. On the platform origin, `/docs` opens the interactive API reference and `/openapi.json` serves its schema. `DOCS_URL` changes the proxy’s docs host; it does not install the docs service or rewrite links in existing client bundles. The SEO tooling’s `TALE_DOCS_URL` and the docs service’s build/runtime path prefix `DOCS_BASE_URL` are separate settings.
Changing `TOTP_ENVIRONMENT` affects newly generated QR codes and setup URIs, including when an existing secret is displayed again. It does not rotate secrets or rename entries already saved on a device; rename those in your authenticator app if needed.
## TLS
| Name | Default | Description |
| ----------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `TLS_MODE` | `selfsigned` | One of `selfsigned`, `letsencrypt`, `external`. See [TLS and domains](https://docs.tale.dev/self-hosted/configuration/tls-and-domains) for trade-offs. |
| `TLS_EMAIL` | unset | Contact email for Let's Encrypt notifications. Optional but recommended in production. |
| `TRUSTED_PROXIES` | `private_ranges` | With `TLS_MODE=external`, the addresses whose forwarded headers the proxy accepts: CIDR ranges separated by spaces, or `private_ranges`. The other modes ignore it. See [TLS and domains](https://docs.tale.dev/self-hosted/configuration/tls-and-domains). |
`selfsigned` runs Caddy with a generated cert — the browser warns, fine for development. `letsencrypt` requires a real domain and ports 80/443 reachable from the public Internet. `external` makes Caddy serve plain HTTP; an upstream reverse proxy terminates TLS.
## Security secrets (required)
| Name | Default | Description |
| ----------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BETTER_AUTH_SECRET` | example value in shipped file | Authentication secret shared by backend replicas. Generate a high-entropy value, for example with `openssl rand -base64 32`. Keep it stable; changing it can invalidate sessions and active sign-in flows. |
| `ENCRYPTION_SECRET_HEX` | example value in shipped file | 32-byte hex encryption root for stored secrets; generate with `openssl rand -hex 32`. Preserve the value matching existing encrypted data. Replacing it does not migrate ciphertext: restore the matching key or re-enter affected secrets through their supported flow. |
| `INSTANCE_SECRET` | example value in shipped file | **Required.** The instance's root secret: 64 hex chars, generated by `tale init` (`openssl rand -hex 32` by hand). At boot the WebDAV app-password HMAC key (`WEBDAV_APP_PASSWORD_HMAC_KEY`) is derived from it unless you set that key yourself, and the short-lived tokens sandbox sessions use to fetch blobs are signed with a subkey of the same derivation. Keep it stable across deploys: rotating it re-derives that key and invalidates every WebDAV app-password. |
| `SANDBOX_TOKEN` | example value in shipped file | **Required.** Shared HMAC secret between the backend and the sandbox spawner: the backend signs every spawner call with it, and the spawner rejects unsigned ones. The spawner refuses to start without it — it holds the host docker socket, so there is no unsigned mode. `tale init` and `bun run dev` mint it; a stack you compose yourself sets it (`openssl rand -hex 32`) before the first boot. Rotating it means restarting the backend and the spawner together — they must agree. |
| `SANDBOX_LLM_GATEWAY_ADMIN_PASSWORD` | unset | **Required for sandbox harness turns.** Gateway management credential used by the backend to provision session keys. Give the gateway the same value: a gateway without an admin account creates one only for a caller that presents it, which is how the backend claims the gateway on first use. The gateway image always uses this value as its setup token (`BIFROST_SETUP_TOKEN`) and replaces one set on the container. The gateway then retains a password hash in `llm-gateway-data`. Keep the matching secret or use the gateway’s supported credential recovery/rotation procedure. Do not wipe its state as routine recovery. The username defaults to `admin` (`SANDBOX_LLM_GATEWAY_ADMIN_USERNAME`). |
Replace the values that ship in `.env.example` before exposing the instance — they are intentionally insecure placeholders.
## Database
Tale keeps two databases: the operational store (`tale_app` — agents, runs, the audit log) and the knowledge corpus (`tale_knowledge` — document chunks, embeddings, crawled pages). A production stack folds both into one ParadeDB service (`db`, port 5432, aliased `knowledge-db`). Both share `DB_PASSWORD`, and the corpus can be pointed at external infrastructure on its own.
| Name | Default | Description |
| ----------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DB_PASSWORD` | `tale_password_change_me` | **Required for bundled Postgres.** Password shared by the application and knowledge databases in the packaged layout. Replace the example value before production. |
| `DATABASE_URL` | constructed from `DB_PASSWORD` | **Optional.** Connection URL for the operational database. Set it to point the backend at a Postgres of your own; it needs no extensions and no superuser, only a database and a role that may create schemas. Read on every start. |
| `DATABASE_POOL_MAX` | `10` | **Optional.** Maximum connections in each operational database pool. Each `backend-api` or `backend-worker` replica normally needs up to twice this value (app pool plus job queue). Sandbox lifecycle operations can temporarily add one connection per occupied app-pool slot, for a peak of three times this value per replica. Count that peak against the database’s `max_connections`. |
| `POSTGRES_CA_FILE` | unset | **Optional.** Path to a PEM bundle trusted for **every** Postgres connection: the operational database, the knowledge corpus, and the databases organizations bring themselves. Needed whenever a URL asks for `sslmode=verify-ca` or `verify-full` against a provider whose root is not one Node ships (Amazon RDS is the common one). Concatenate several roots into one file if your databases use different providers. |
| `KNOWLEDGE_DATABASE_URL` | `postgresql://tale:${DB_PASSWORD}@knowledge-db:5432/tale_knowledge` | Connection URL for the default knowledge corpus. Pointing it elsewhere selects another database; it does not migrate existing chunks or vectors. |
| `KNOWLEDGE_DB_POOL_MAX` | `10` | **Optional.** Connections one backend process opens to the knowledge corpus. Every indexing job holds one while it commits a slice of chunks, so a worker allowed more concurrent jobs than this (`WORKER_CONCURRENCY`) queues on the pool — raise the two together. Like `DATABASE_POOL_MAX`, it counts per replica against the corpus database's `max_connections`. |
| `KNOWLEDGE_DB_NAME` | `tale_knowledge` | Name of the knowledge database created by the bundled database initialization. |
| `KNOWLEDGE_INDEX_REPAIR_INLINE_MAX_BYTES` | `1073741824` | **Optional.** Largest BM25 search index (in bytes) the backend rebuilds synchronously at boot when it finds it corrupted; a larger one is rebuilt by a background job while writes to that corpus are refused. See [Container architecture](https://docs.tale.dev/self-hosted/operate/container-architecture). |
| `KNOWLEDGE_INDEX_REPAIR_DISABLED` | unset | `1` or `true` disables automatic boot-time BM25 verification and repair. It does not fix corruption; failed queries or writes need investigation and a controlled repair. |
| `KNOWLEDGE_CRAWL_DOCUMENT_MAX_BYTES` | `104857600` | **Optional.** Largest document (PDF, DOCX, XLSX, PPTX, ODT — in bytes) the website crawler downloads and extracts; a larger one is recorded on its page as too large to fetch. HTML pages have their own 2 MiB budget. Every fetch is held in memory while its text is extracted, and up to five sites scan at once, so size the backend's memory with it. A value below 1 MiB, or not a whole number, falls back to the default. |
The auto-constructed operational form is `postgresql://tale:${DB_PASSWORD}@db:5432/tale_app` (override the database name with `APP_DB_NAME`). The knowledge corpus lives in `tale_knowledge` with the `private_knowledge` and `public_web` schemas. These variables set the deployment defaults every organization shares; an organization can additionally point its own corpus and its own bucket at infrastructure of its own under **Settings > Data residency** (per-organization files, applied live, no restart), covered in [Data residency](https://docs.tale.dev/self-hosted/configuration/data-residency).
Two things to know before pointing either database at infrastructure of your own:
- **The knowledge corpus needs `pgvector` installed.** Tale creates its schemas and tables on a database that starts empty, but it never installs extensions — the chunk table has a `vector` column, so `CREATE EXTENSION vector;` has to have been run on the target database first. ParadeDB's `pg_search` is optional: without it, search degrades to vector-only rather than failing. The operational database needs no extensions at all.
- **Use a direct or session-compatible Postgres connection.** Job notifications, migration locks, and prepared statements need session semantics. Validate any managed connection proxy against those requirements.
## Object store
Files and media use S3-compatible storage. These variables configure the deployment default. An organization’s explicit connection takes precedence; an unavailable default does not imply that every organization’s own bucket is unavailable.
| Name | Default | Description |
| -------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OBJECT_STORE_ACCESS_KEY` | unset | Access key for the deployment’s default S3 connection. For bundled MinIO, configure the same value as `MINIO_ROOT_USER`. Missing credentials do not create a default connection; an organization can still have its own valid connection. |
| `OBJECT_STORE_SECRET_KEY` | auto-generated by `tale init` | Secret key for the default store. For bundled MinIO, it must match `MINIO_ROOT_PASSWORD`. Rotate the store and backend credentials together and verify reads and writes; changing a password does not migrate or inherently orphan blobs. |
| `OBJECT_STORE_BUCKET` | `tale-blobs` | Bucket blobs are stored in. Created if it is absent and the key is allowed to; an existing bucket is used as it is. |
| `OBJECT_STORE_ENDPOINT` | `http://object-store:9000` in the shipped compose | Backend endpoint for the store. AWS S3 uses no custom endpoint; remove or explicitly clear the bundled endpoint in your effective Compose environment. Set a custom URL for MinIO, R2, or another compatible service. |
| `OBJECT_STORE_REGION` | `us-east-1` | Signing region. Meaningful for AWS; arbitrary but required by the signer for a self-hosted store. |
| `OBJECT_STORE_FORCE_PATH_STYLE` | `true` with an endpoint, `false` without | Address the bucket as `endpoint/bucket/key` rather than `bucket.endpoint/key`. The default follows the endpoint, which is right for both the self-hosted case and AWS; set it only for a store that disagrees with its own shape. |
| `OBJECT_STORE_PREFIX` | unset | Key prefix inside the bucket, so Tale's blobs can share a bucket with other data. Empty means the bucket root. |
| `OBJECT_STORE_PUBLIC_ENDPOINT` | `${SITE_URL}` (set by the CLI) | Where the **browser** reaches the store. The proxy publishes the bundled store at `//*` and forwards presigned URLs verbatim, so uploads and downloads run browser↔store directly. When this endpoint is one of the deployment's origins, a link for a browser on another configured origin is signed for that origin. Leave it unset for a bucket the browser can already reach. |
The packaged proxy exposes the bundled store’s object route to browsers without publishing the store’s administration port. An external store can be reached directly through its configured public endpoint.
### How these variables reach the running deployment
At startup, the backend reconciles `default/object-storage/connection.json` with its environment. Apply changed values by recreating `backend-api` and `backend-worker`. The startup messages identify these outcomes:
| Line | Meaning |
| --- | --- |
| `object store (seeded)` | there was no connection; one was written from the environment |
| `object store (reconciled)` | the environment changed; the connection was updated to match |
| `object store (adopted)` | a connection written by an older release was recognised and is now kept in step |
| `object store (ignored)` | the connection is marked `"managedBy": "operator"`, so these variables do nothing |
| `object store (skipped)` | No credential pair was supplied to create a deployment default. Check whether a usable existing or organization connection remains. |
| *(nothing)* | already in step — the steady state |
To manage the store by hand instead, set `"managedBy": "operator"` in `connection.json`; the backend then never touches that file. A file with no `managedBy` at all — written before this behaviour existed — is taken over only if it still names the same bucket at the same endpoint the environment does; if you had repointed it by hand, that edit is kept.
Bucket permissions: the backend checks the bucket exists (`HeadBucket`) and creates it only if it does not. A key that may read, write and delete objects but not create buckets is therefore fine, as long as you create the bucket yourself. Presigned uploads and downloads run in the browser, so an external bucket also needs a CORS policy allowing your deployment's origin with `GET`, `PUT` and `HEAD` — see [Data residency](https://docs.tale.dev/self-hosted/configuration/data-residency).
## Audit log privacy
A pepper pseudonymizes personal data recorded after failed sign-ins. Earlier releases also generated `TALE_AUDIT_SIGNING_KEY` and `TALE_AUDIT_SIGNING_KEY_PREVIOUS`. Nothing reads either, so you can keep or delete an existing value.
| Name | Default | Description |
| --------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TALE_AUDIT_PEPPER` | auto-generated by `tale init` | At least 16 characters for failed-sign-in pseudonymization: HMAC-SHA256 of email and truncated IP address. Without it, these audit fields retain plaintext values and the backend warns. Rotation breaks correlation with earlier identifiers; retention follows each organization’s applied policy. |
See [Audit log integrity](https://docs.tale.dev/self-hosted/operate/security/audit-log-integrity) for the verification model.
## Observability
| Name | Default | Description |
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `SENTRY_DSN` | unset | Sentry DSN for error tracking. Leave unset to disable. Compatible with self-hosted GlitchTip and Bugsink. |
| `SENTRY_ENVIRONMENT` | unset | Shared reporting label for browser, backend and sandbox events. Managed deployments default to their retained `name`; an explicit environment reference can select a canonical label without renaming the deployment. |
| `SENTRY_TRACES_SAMPLE_RATE` | unset | Optional sample rate for browser performance traces (`0.0`–`1.0`); independent of backend sampling. |
| `BACKEND_SENTRY_TRACES_SAMPLE_RATE` | `0` | Backend HTTP and worker span sample rate (`0.0`–`1.0`). Requires `SENTRY_DSN` and a destination accepting Sentry transactions; `0` disables spans. |
| `METRICS_BEARER_TOKEN` | unset | Bearer token for the proxy’s `/metrics/*` routes. Without a configured token they return 401. Internal process endpoints remain a separate network-access concern. |
| `UMAMI_URL` | unset | HTTPS origin of the authenticated collector gateway. HTTP is accepted only for local testing on `localhost`, `127.0.0.1` or `[::1]`. Requires a valid website ID and proxy token; no path, query or credentials in this URL. |
| `UMAMI_WEBSITE_ID` | unset | Umami website UUID. Unset or invalid disables aggregate analytics; use a separate ID for each deployment. |
| `UMAMI_PROXY_TOKEN` | unset | Server-only bearer token for the collector gateway: 16–256 ASCII letters, digits or characters from `._~-`. Never inject it into browser configuration. |
Setting `METRICS_BEARER_TOKEN` exposes the metrics endpoints behind the token: `/metrics/platform`, `/metrics/backend` (the application backend's metrics), and `/metrics/sla-rules`. See [Observability config](https://docs.tale.dev/self-hosted/configuration/observability-config) for the scrape config.
## Provider secrets encryption
SOPS protects supported configuration secret sidecars. Current provider credentials in the database use `ENCRYPTION_SECRET_HEX`, listed with the security secrets above.
| Name | Default | Description |
| --- | --- | --- |
| `SOPS_AGE_KEY` | unset | One inline private age key. Takes precedence over the key-file setting. |
| `SOPS_AGE_KEY_FILE` | unset | Path visible to the consuming process, containing one or more private age keys, one per line. Mount the file into each container that needs it. |
Without an age key, the SOPS helper writes supported sidecars as plaintext at mode `0600`; existing encrypted files still require their key. Read [Secrets with SOPS](https://docs.tale.dev/self-hosted/configuration/secrets-with-sops) before changing either variable.
Provider credentials may instead reference a variable under `TALE_PROVIDER_KEY_` (maximum name length 40). Subscription-broker credentials use the separate `TALE_TOKEN_SOURCE_` prefix (maximum 60). These fields store variable names, not secret values. Inject values into the backend processes and recreate the consumers when values change. [Providers](https://docs.tale.dev/self-hosted/configuration/providers) describes this setup.
## Connector OAuth apps
OAuth connectors (Gmail, Google Drive, Outlook, Teams, Slack, …) resolve their vendor app per organization first: an app configured under **Settings > Connectors > OAuth apps** wins for that org. The environment supplies the deployment-wide default underneath (and is the only source for Slack, whose inbound event verification runs before any org is known). For each connector slug:
| Name | Default | Description |
| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `CONNECTOR_OAUTH__CLIENT_ID` | unset | OAuth client ID for that connector. Slug is upper-cased with dashes as underscores (`gmail` → `GMAIL`). |
| `CONNECTOR_OAUTH__CLIENT_SECRET` | unset | Matching client secret. |
| `CONNECTOR_SLACK_SIGNING_SECRET` | unset | The Slack app's signing secret. The inbound Events endpoint verifies every delivery with it and answers 503 while it is unset. |
Register `${SITE_URL}${BASE_PATH}/api/connectors/oauth2/callback` on the vendor app, and for Slack also `${SITE_URL}${BASE_PATH}/api/connectors/slack/events` as the Events Request URL. Details: [Connectors (develop)](https://docs.tale.dev/develop/connectors).
## Knowledge cloud import (Documents)
Per-user OneDrive / Google Drive authorizations for **Knowledge → Documents** are separate from org connectors and from login. An org-level app configured under **Settings > Connectors > OAuth apps** takes precedence here too — the **google-drive** entry is shared with the connector lane, and **OneDrive / SharePoint (Knowledge import)** has its own entry. The chains below resolve wherever the org has not configured one. Register this redirect URI on the Microsoft (or Google) app:
`${SITE_URL}${BASE_PATH}/api/cloud-import/oauth2/callback`
Credential resolution for OneDrive (first match wins):
| Name | Description |
| ---------------------------------------------- | ------------------------------------------- |
| `CLOUD_IMPORT_MICROSOFT_CLIENT_ID` / `_SECRET` | Dedicated Knowledge import app (preferred). |
| `CLOUD_IMPORT_MICROSOFT_TENANT_ID` | Directory (tenant) ID for that app. |
| `AUTH_MICROSOFT_ENTRA_ID_ID` / `_SECRET` | Login Microsoft app. |
| `AUTH_MICROSOFT_ENTRA_ID_TENANT_ID` | Directory (tenant) ID for the login app. |
Single-tenant Entra app registrations must use a tenant-specific authorize URL — `/common` fails with AADSTS50194. Set the tenant ID (or `organizations` / `common` for a multi-tenant app). When unset, Tale falls back to the org's Entra SSO issuer tenant if configured.
The Microsoft consent screen requests Graph **Files.Read** and **Sites.Read.All** (list/download OneDrive and SharePoint), **User.Read** (account label), and **offline_access** (refresh token for sync). That grant is intentional and per user — it is not attached by signing in to Tale.
Google Drive uses a dedicated app only (no login-app fallback):
| Name | Description |
| ------------------------------------------------- | ---------------------------------- |
| `CLOUD_IMPORT_GOOGLE_DRIVE_CLIENT_ID` / `_SECRET` | Knowledge Google Drive import app. |
Register the same cloud-import callback URI on the Google OAuth client. Consent requests **drive.readonly** and **userinfo.email**.
## Feature flags
These variables configure backend authentication, file events, and operator permissions. Recreate the consuming backend roles when their environment changes; changing only the web container is insufficient.
| Name | Default | Description |
| --------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRUSTED_SECRET_HEADER` | `Remote-Internal-Secret` | Name of the request header that carries the organization's trusted-header key on the hand-off request. |
| `TRUSTED_EMAIL_HEADER` | `Remote-Email` | Name of the request header carrying the user's email — the identity the session is minted for. |
| `TRUSTED_NAME_HEADER` | `Remote-Name` | Name of the request header carrying the display name. Falls back to the local part of the email. |
| `TRUSTED_ROLE_HEADER` | `Remote-Role` | Name of the request header carrying the organization role the session acts with, capped at the organization's ceiling (`member` when the header is absent). |
| `TRUSTED_TEAMS_HEADER` | `Remote-Teams` | Name of the request header carrying team memberships as comma-separated team names (`id:name` entries are accepted too). Absent = teams untouched; present = the proxy's list is authoritative for the memberships it granted (empty revokes them). |
| `TALE_FILE_EVENTS` | `false` | Streams config-file changes under `TALE_CONFIG_DIR` to open browser tabs (`/events/file`), so an agent, skill, or branding file edited on disk shows up without a reload. On in the dev compose, off in production. |
| `TALE_DEPLOYMENT_CONFIG_ADMINS` | unset | Comma-separated email allowlist of operators allowed to write the deployment config file (`deployment.yml`, today the sandbox runtime section) through the API. Empty/unset = read-only for all admins. Data residency is configured per organization and is not gated by this list. |
| `TALE_ALLOW_PRIVATE_PROVIDER_HOSTS` | unset | Set to `1` in the backend environment to admit private model-provider destinations, including their sandbox gateway configuration. Cloud-metadata targets remain blocked. See [Providers](https://docs.tale.dev/self-hosted/configuration/providers). |
| `TALE_ALLOW_PRIVATE_CRAWL_HOSTS` | unset | Set to `1` to admit intranet crawl targets and private product `imageUrl` hosts. Cloud-metadata targets remain blocked. |
| `TALE_ALLOW_OPEN_SIGN_UP` | unset | Set to exactly `true` to keep `POST /api/auth/sign-up/email` open after the deployment's first account. For throwaway test stacks only — the local dev orchestrator and the dev compose overlay set it themselves. A real deployment leaves it unset, so an administrator creates every account after the first. |
| `TALE_ORGANIZATION_CREATORS` | unset | Comma-separated email allowlist of the accounts that may create an organization, matched case-insensitively. Unset, every signed-in user may create one. Set, every other user is refused with `403 ORGANIZATION_CREATION_FORBIDDEN` once the deployment holds an organization — the first one is always allowed — and the app hides **Create organization** from them. A set but empty value closes creation to everyone. A managed deployment writes it from `organizations.creators` in its specification; see [Install the tale CLI](https://docs.tale.dev/self-hosted/install/cli-install#managed-organization-creators). |
The private-crawl opt-in affects two boundaries: website registration and crawler requests, and validation of a product’s `imageUrl`. Without it, a private website target returns `400 WEBSITE_DOMAIN_NOT_CRAWLABLE`; a private product image URL returns `400 INVALID_BODY`. Product validation checks the hostname string without fetching the image or resolving DNS. Website registration and crawling also check resolved addresses. Enable the flag only for a deployment that needs these private destinations; it is separate from the private-provider flag.
## Deployment topology
These values shape the application roles of a workspace deployment: the replica counts, which `tale deploy` reads from the project environment and clamps to the supported range with a warning, and how much work one worker replica takes on at once.
| Name | Default | Description |
| ------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------- |
| `TALE_PLATFORM_REPLICAS` | `1` | Replicas of the web tier that serves the app shell. Range `1`–`16`. |
| `TALE_BACKEND_API_REPLICAS` | `1` | Replicas of the API — every application door, auth, and the hint stream. Range `1`–`16`. |
| `TALE_BACKEND_WORKER_REPLICAS` | `1` | Replicas of the job runner: ingestion, crawls, automations, agent turns. Range `1`–`16`. |
| `WORKER_CONCURRENCY` | `5` | Jobs one `backend-worker` replica runs at once on each job queue — ingestion, crawls, automations and agent turns each get this many. Agent turn starts get at least 8 and the drive windows of live agent turns at least 16 per queue, since each one mostly waits on its sandbox (`AGENT_START_SLOTS`, `AGENT_DRIVE_SLOTS`). Read by the worker process itself; range `1`–`64`. The lever to pull before adding worker replicas when a backlog lags. Every running indexing job commits through the knowledge pool, so raise `KNOWLEDGE_DB_POOL_MAX` with it. |
| `AGENT_START_SLOTS` | `WORKER_CONCURRENCY`, at least `8` | Agent turn starts one `backend-worker` replica runs at once, for task runs and for automation steps each. A start holds its slot through the sandbox create and the turn's first 90-second window. Range `1`–`256`. |
| `AGENT_DRIVE_SLOTS` | `WORKER_CONCURRENCY`, at least `16` | Drive windows of live agent turns one `backend-worker` replica runs at once, per lane. A replica keeps about 2.5 times this many live turns per lane drained; past that, windows wait long enough for recovery to step in, so raise it (or add replicas) for more turns at once. Range `1`–`256`. |
| `TALE_BACKEND_URL` | `http://backend-api:3005` | Where the web tier reaches the application backend: the public `/status` page probes it and the web server asks it for the answers only a database can give. The shipped compose and the container entrypoint default it to the in-compose alias; set it only when your backend service has another name. Read by the `platform` service only. |
A workspace rollout temporarily runs both colors. Plan capacity for that overlap. Increase the role whose measured workload is the bottleneck; more replicas also increase database connections and memory use. See [Upgrades](https://docs.tale.dev/self-hosted/operate/upgrades).
## Sessions
| Name | Default | Description |
| ------------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SESSION_IDLE_TIMEOUT_MINUTES` | unset | **Optional.** Sign a session out after this many minutes of inactivity (`1`–`1440`). The window slides on activity and is enforced server-side across email/password, SSO, and trusted-headers sessions. |
Leave it unset to keep the default session lifetime. When set, an idle session expires server-side once the window elapses, while an active one keeps sliding forward on each request. Org admins can tighten the effective window per organisation — never loosen it past this cap — via the [session idle timeout governance policy](https://docs.tale.dev/platform/admin/governance/policies-and-limits); idle sessions under that policy are revoked by a sweep that runs about every five minutes.
## Support link
| Name | Default | Description |
| -------------------------- | -------------------------- | ----------- |
| `TALE_CONTACT_SUPPORT_URL` | `https://tale.dev/contact` | **Optional, read by the `platform` service.** Where the **contact support** link on the app's error screens points. An absolute `http://` or `https://` URL. |
Set it to your own help desk so people who hit an error reach the team that runs your deployment. Where the error screen knows the organization, the link adds `organizationId=` to the query string, after any query the URL already carries, and replaces an `organizationId` the URL already has. Any other value, such as `mailto:` or a URL without a scheme, is ignored with a warning in the `platform` service's log, and the link keeps the default.
## Sandbox infrastructure
The sandbox spawner reads the settings below. Pass them into its environment and recreate that service after a change. `SANDBOX_MAX_SESSIONS` sets the capacity shared by all organizations; an organization's three workload limits add up automatically and cannot be saved above that capacity. Manage those limits in [Sandboxes](https://docs.tale.dev/platform/admin/sandboxes), where actual runtime counts and host measurements appear separately from workload allocations.
| Name | Default | Description |
| --- | --- | --- |
| `SANDBOX_MAX_SESSIONS` | sized from host memory on a local Docker host (at least `8`, at most `256`); `8` elsewhere | Maximum running and starting sessions across all organizations on the Docker host or in the Kubernetes namespace, including idle containers kept for reuse. This capacity does not reserve CPU or memory. Unset on a Docker host whose memory the spawner can read, it is sized from that memory: one session per 768 MiB left after `SANDBOX_MIN_FREE_MEMORY`, or per 1.5 GiB where agent sessions run Docker inside the sandbox. If the memory cannot be read at start, the capacity stays `8` until it can. An explicit value fixes it. Concurrent Kubernetes replicas enforce it on a best-effort basis; use ResourceQuota for hard namespace resource bounds. |
| `SANDBOX_MIN_FREE_MEMORY` | a tenth of host memory, at least `1g` | Memory a new session or a released warm session resuming work must leave free on a local Docker host, beside what the sessions still starting or just started are about to use. A create that would leave less first stops a released idle session; if memory is still short, the spawner answers busy and the work waits until running sessions free some. Accepts sizes such as `2g` or `1536m`. Has no effect on Kubernetes or with a remote Docker daemon. |
| `SANDBOX_MIN_FREE_DISK` | a twentieth of each disk, at least `2g`, at most `20g` | Free space required for new and reactivated sessions on the workspace filesystem and the verified Docker data filesystem. Below the floor work waits; stopped unused build caches are reclaimed first. Accepts `10g` or `4096m`; `0` disables the check. Kubernetes relies on its storage class and volume provisioning. |
| `SANDBOX_DOCKER_WORKLOADS` | `project,workflow` | Workloads allowed to use inner Docker when it is enabled for the deployment. Set `project` to allow Docker only for project agents, `workflow` only for workflow agents, or `none` to disable both. Applies to newly created sessions; existing sessions keep their capabilities. A session request can opt out with `docker: false`, but cannot enable Docker beyond this allowlist or the deployment setting. |
| `SANDBOX_AGENT_CPUS` | `2` | CPU limit per agent session. Account for overlapping builds and other host workloads when choosing the session count. |
| `SANDBOX_AGENT_MEMORY` | `4g`; `8g` with Docker inside the sandbox | Memory limit per agent session, shared with its inner Docker daemon and nested containers. An explicit value overrides either default and applies to newly created sessions. |
| `SANDBOX_AGENT_PROFILE` | `agent` | Read by the backend API and worker. Profile for new agent and workflow workspaces: `agent` supports inner Docker; `agent-light` keeps coding tools and the persistent workspace without starting Docker or build-cache helpers. Existing workspaces retain their recorded profile. |
| `TALE_SANDBOX_CLAUDE_EFFORT` | unset (inherits the harness default) | Read by the backend API and worker. Claude Code reasoning effort: `low`, `medium`, `high` or `max`. Lower settings omit the automatic Ultrathink instruction and set the per-exec effort, while keeping adaptive thinking enabled. Other harnesses and foreign models are unaffected. Compare representative tasks before lowering it; shorter runs are not guaranteed. |
| `SANDBOX_SESSION_CREATE_TIMEOUT_MS` | `180000` | Total session startup budget, including optional build-cache setup, container or Pod startup, daemon readiness and environment delivery. A cancelled request aborts its session startup work; cleanup stops compute while preserving the workspace. Shared cache provisioning has its own bounded lifetime. |
| `SANDBOX_DOCKER_DATA_ROOT` | unset | Optional host path matching Docker's `DockerRootDir`. The CLI adds a read-only bind mount to the sandbox service when configured. Raw Compose requires the mount shown below. |
| `SANDBOX_DOCKER_DATA_PATH` | `/var/lib/tale-sandbox/docker-data` when the root is configured | Location of that read-only mount inside the spawner. Admission checks this filesystem as well as the workspace filesystem. A configured mount that cannot be verified or read blocks admission. |
| `SANDBOX_BUILDKITD_CPUS` | `SANDBOX_AGENT_CPUS` | CPU limit of an organization's build-cache helper, which runs the image builds of all its agent sessions when Docker runs inside the sandbox. A running helper takes a new value at once. The number of concurrent BuildKit solver steps is this CPU limit rounded down, with a minimum of one; this bound takes effect when an idle helper is recreated. |
| `SANDBOX_BUILDKITD_MEMORY` | twice `SANDBOX_AGENT_MEMORY` | Memory limit of an organization's build-cache helper, shared by the builds its agent sessions run at the same time. Accepts sizes such as `16g` or `12288m`. A running helper takes a new value when it is recreated, once no build is running. |
| `SANDBOX_BUILDKITD_IDLE_CACHE` | `5g` | Build cache an organization's build-cache helper keeps when it stops for want of agent sessions. Right before that stop, the spawner prunes the cache down to this size, least recently used first: the cache's own garbage collection runs only while the helper runs. Accepts sizes such as `5g` or `2048m`; `0` keeps nothing. |
| `SANDBOX_BUILDKITD_PROVISION_TIMEOUT_MS` | `5000` | Total time in milliseconds for optional shared build-cache setup during session creation, including queued Docker calls and registry mirrors. Range `100`–`60000`. When the shared setup budget expires, queued helper work is cancelled and cannot launch later; the session falls back to its local builder. |
| `SANDBOX_BUILDKITD_CACHE_RETENTION` | `14d` | How long an organization's stopped build-cache helpers keep their caches. Once the builder has been stopped longer than this and no session of the organization may use it, the spawner removes its helpers, network and cache volumes, as an organization's deletion does; its next build starts cold. Accepts whole days or hours such as `14d` or `336h`; `off` (or `0`) keeps the caches until the organization is deleted. |
| `SANDBOX_SESSION_MAX_IDLE_MS` | `1800000` (30 min) | Idle window for stopping unpinned sessions. Organization build-cache helpers also stop after this window with no potentially active organization session; their networks and cache volumes are retained. |
| `SANDBOX_SESSION_RELEASED_IDLE_MS` | `300000` (5 min) | Idle window for a released session: one whose task or run has finished and that no other work holds. An unpinned released session idle this long is stopped instead of waiting out `SANDBOX_SESSION_MAX_IDLE_MS`; the next turn resumes it on its preserved workspace. Agent sessions with Docker inside the sandbox keep the full idle window, because each resume starts their inner Docker daemon with an empty image store. |
| `SANDBOX_RUNTIME_IMAGE` | `tale-sandbox-runtime:latest` | **Optional, read by the spawner.** The image every session container is created from. The default is the tag the development stack builds locally, so a host that pulls its images sets the registry one: `ghcr.io/tale-project/tale/tale-sandbox-runtime:`, matching the rest of the stack. `tale deploy` sets it for you. |
| `SANDBOX_DIND_INNER_POOL` | unset (automatic) | Optional inner Docker address pool for agent sessions on Docker or Kubernetes. Use a canonical RFC1918 IPv4 `/16` outside your Pod, Service and VPC networks. The runtime rejects overlaps with networks and addresses it discovers. |
| `SANDBOX_EGRESS_MAX_CLIENTS` | `2000` | **Read by the `sandbox-egress` service**, not the spawner: recreate that service after a change. Connections the egress proxy serves at once for all sessions together; with transparent egress, every outbound connection of a session, its builds and its package installs goes through it. Past the limit the proxy refuses new connections, and installs and page loads fail with connection resets. Each connection takes a thread and two open files, so the shipped compose files size the proxy's process and open-file limits for the default; a higher value needs them raised too, and the proxy warns at start when its open-file limit is too low. At start the proxy raises its open-file limit to what the connections need, as far as the container's hard limit allows. On Kubernetes, set it on the egress Deployment; a Pod spec sets no process limit, so the node's `podPidsLimit` must leave room for one thread per connection. |
The backend settings above must reach both API and worker services. Recreate them after a change. The spawner checks memory and disk headroom when a released session is acquired again, so a warm workspace can wait for capacity too.
Connected devices apply the same local-host memory and disk admission checks while keeping their configured session ceiling.
An intentionally stopped Docker daemon remains ready for its next command; health checks do not start it. A failed health probe immediately refuses new work but does not by itself stop the session. Probe-based recovery needs at least three completed failures spanning five seconds; a healthy result or a new engine clears that failure history. A confirmed startup failure or unexpected engine exit can trigger recovery immediately. The spawner can then stop an idle, unpinned session after an atomic activity check, keeping the workspace for the next start. On Docker, stopping the whole session removes its inner Docker store; stopping only the idle inner engine preserves it. Busy or pinned sessions stay in place and refuse new starts until Docker recovers or an operator resolves the fault.
During a rolling upgrade, keep old spawners pinned to their existing runtime image until they are replaced. Do not move a runtime tag still used by an old spawner: it does not distinguish transient probe failures from confirmed failures. A new spawner with an older runtime refuses unhealthy new work and keeps the normal idle and lifetime cleanup limits.
A containerized spawner automatically checks Docker’s data filesystem when it can verify its existing `/etc/hostname` mount against the daemon. If discovery is unavailable, workspace admission remains active and `/health` reports Docker disk monitoring as `unavailable`. An explicit read-only data-root mount takes priority; if it cannot be verified or read, admission waits until it is available. To configure one, inspect `docker info --format '{{.DockerRootDir}}'` and use that actual host path. Docker Desktop and remote daemons may need a different setup. For raw Compose, add the following override and pass both variables into the sandbox service:
```yaml
services:
sandbox:
environment:
SANDBOX_DOCKER_DATA_ROOT: ${SANDBOX_DOCKER_DATA_ROOT}
SANDBOX_DOCKER_DATA_PATH: /var/lib/tale-sandbox/docker-data
volumes:
- ${SANDBOX_DOCKER_DATA_ROOT}:/var/lib/tale-sandbox/docker-data:ro
```
A disk floor is admission control, not a per-session storage quota. Docker named volumes have no portable size limit, and a Kubernetes PVC size request depends on its storage backend for enforcement. For hard quotas, use a quota-capable filesystem or storage class. Separately mounted containerd stores, volume directories and external volume drivers need their own monitoring, alongside workspaces, image layers and build caches.
Kubernetes sessions use `/readyz` for startup and `/livez` for daemon liveness. Startup accepts an intentionally stopped inner Docker engine as ready; an engine that is running must respond to its health probe. Liveness checks only the sandbox daemon, so dependency failures do not restart busy sessions. A wedged daemon can restart even when its session is pinned. Docker and egress diagnostics appear in `/healthz`.
An already active generation is not charged again. A workflow without inner Docker keeps its agent identity and tools, but starts no Docker daemon and does not keep build-cache helpers alive. Without an explicit `SANDBOX_AGENT_MEMORY`, each session defaults to `4g` without inner Docker or `8g` with it, even when other sessions in the deployment use Docker. An explicit value applies to both modes. These limits are ceilings, not reserved RAM.
At full capacity, the spawner can stop a released, unpinned idle session before the idle timeout to admit new work, starting with the one idle longest. The daemon must confirm that no work is in progress; busy sessions and sessions with unknown state are protected. Stopping compute preserves the persistent workspace directory or volume. If no safe session can be reclaimed, admission remains blocked by the deployment capacity. The same applies when the host's free memory would drop below `SANDBOX_MIN_FREE_MEMORY`. While the disk that holds the session workspaces is below `SANDBOX_MIN_FREE_DISK`, new work waits without stopping idle sessions: a stopped session keeps its workspace.
Harness protocol records, including unfinished lines, are limited to 8 MiB. Oversized or incomplete output fails explicitly; a bounded progress preview is never treated as a complete structured result. Complete final answers are retained separately from that preview.
Reacquiring a released warm session checks the same memory reserve and reserves its expected working set before resuming work. Concurrent resumes share that accounting. A busy response is retryable; it does not destroy the workspace or interrupt a session that already holds work. These reservations do not cap the later growth of active tools or organization builders.
When Docker is enabled inside agent sandboxes, the engine starts automatically on the first Docker command. Agents do not choose a mode. After five minutes without connected clients, it stops only if no container is running, restarting or paused and every container has its restart policy disabled; an unknown state keeps it running. The next command restarts it with the same images, volumes and workspace. Existing container metadata at session-container boot starts the engine immediately so restart policies still work. This does not change the deployment's runtime isolation, privileges or memory limits.
### Size session capacity
On a Docker host with `SANDBOX_MAX_SESSIONS` unset, the spawner sizes the capacity from the host's memory (a 16 GiB host gets 19 sessions, a 64 GiB host 76; 9 and 38 where agent sessions run their own Docker daemon) and admits a new session only while the host keeps `SANDBOX_MIN_FREE_MEMORY` free, counting what the sessions still starting or just started are about to use. A burst of new sessions that would overcommit the host therefore waits instead of starting; the check does not stop sessions already running from growing. Set the capacity yourself on Kubernetes, with a remote Docker daemon, or when the host also runs other heavy workloads: test the tasks your deployment will run together. Browser rendering and Docker builds have different peaks; include agents, workflows and crawling across all organizations. A free session slot does not guarantee enough resources.
On Docker, sample resource use while representative tasks overlap. Repeat this command during the run; an idle snapshot does not show task peaks:
```bash
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
```
Subtract the operating system, platform services, databases, build-cache helpers and a safety margin from host memory. Divide the remaining memory by the measured peak per active session and round down. For example, a 32 GiB host with 8 GiB set aside and a measured peak of 3 GiB per session gives `(32 - 8) / 3 = 8` sessions. This is a sizing example, not a benchmark. For mixed workloads, budget their simultaneous peaks together and check CPU saturation and task duration before raising the limit.
An agent's 4 GiB or 8 GiB memory limit is a ceiling, not memory reserved at startup. Eight agents running Docker builds can therefore require much more memory than eight mostly idle sessions. Measure under representative load and keep headroom; increase capacity to 16 or higher only when the host can sustain it. Before lowering capacity, reduce any organization totals that exceed the new value. The default organization limits total 6, so a smaller deployment capacity also needs smaller organization limits.
### Apply a capacity change
Add or update this line in the deployment's `.env`, keeping its other entries. Explicit values remain in effect across upgrades; without one, a Docker host sizes the capacity from its memory as described above, and Kubernetes uses 8.
```dotenv .env
SANDBOX_MAX_SESSIONS=8
```
For a Compose stack you manage yourself, recreate only the sandbox service using its existing local image:
```bash
docker compose up -d --no-deps --no-build --pull never sandbox
```
Use the same project, `-f` files and environment-file options as the running stack. A restart alone does not load an edited `.env`. For CLI-managed installations, apply the change through the deployment workflow in [Upgrades](https://docs.tale.dev/self-hosted/operate/upgrades). On Kubernetes, set the variable on the sandbox spawner Deployment and roll out that Deployment.
Confirm the new deployment capacity in [Sandboxes](https://docs.tale.dev/platform/admin/sandboxes). With organization limits at 2/2/2 and deployment capacity at 8, the total reads **6 / 8**. Existing organization settings are preserved; saving still requires their total to fit the current capacity. Changing the capacity does not increase any container's CPU or memory limit.
### After an upgrade
The default capacity used to be 16, and organizations created before this release were seeded with limits of 2/4/4, a total of 10. A deployment that never set `SANDBOX_MAX_SESSIONS` therefore starts the new version with a capacity of 8 and organizations whose saved total exceeds it. Running work is not affected, and each organization keeps admitting work under its saved limits; only saving the Sandboxes page is blocked until its total fits, and lowering limits still saves. Either set `SANDBOX_MAX_SESSIONS=16` explicitly to keep the previous capacity, or ask each affected organization to lower one limit.
From this release, a Docker deployment that never set `SANDBOX_MAX_SESSIONS` gets a capacity sized from its host's memory instead of 8, never less than 8. It can admit more sessions at once than before, and only while the host has memory to spare. Set an explicit value to keep a fixed capacity.
### Docker build caches
Shared build-cache preparation uses `SANDBOX_BUILDKITD_PROVISION_TIMEOUT_MS` (5 seconds by default). Each session waits at most that long or one quarter of its total startup budget, whichever is shorter, then uses its local builder. Shared preparation has its own bounded lifetime; cancelling one session’s wait does not cancel setup for other sessions, and a late result does not attach to a session that already fell back. Registry mirrors are prepared concurrently. The shared builder’s parallel steps follow its configured CPU limit, rounded down with a minimum of one; its memory limit still applies. Preparation time counts against the remaining session startup budget.
Docker build caches are isolated by organization. Each organization uses one privileged builder and three unprivileged registry mirrors. Once no session may still use them, the helpers stop after the session idle window; the next build restarts them with their cache volumes intact. Right before the builder stops, the spawner prunes its cache to `SANDBOX_BUILDKITD_IDLE_CACHE`, because its garbage collection runs only while it runs. A stopped organization's cache is not pruned further; after `SANDBOX_BUILDKITD_CACHE_RETENTION` (14 days) with no build, its helpers and caches are removed, and otherwise they go with the organization. While the session disk is below `SANDBOX_MIN_FREE_DISK`, stopped organizations' caches go sooner, the longest-stopped first. Networks and volumes remain available for reuse. Kubernetes sessions use their own inner Docker builder; Kubernetes reconciliation does not invoke the Docker CLI for these helpers.
The spawner packs organization bridges into the first available Docker address pool before moving to the next. Its default `/23` gives each bridge 512 addresses; an otherwise unused `/16` holds 128 such organization networks. Smaller subnets configured in Docker’s pools remain smaller. It excludes existing Docker networks, routes and DNS server addresses on the Docker daemon’s host, and `172.31.0.0/16` for older runtime images, then validates the created network.
To observe the daemon’s host even with remote Docker, the spawner briefly runs the configured BuildKit image in the host network namespace with a read-only filesystem, all capabilities dropped and no mounts. If that observation fails or no safe subnet remains, sessions build locally without the shared cache. An unused owned network left with an invalid subnet is rebuilt; in-use and foreign networks are preserved.
Upgrades create cold organization caches and retain the old global cache data. Old helper containers stop automatically after no running session depends on them. Drain or stop old pinned sessions to complete that transition; until then the old shared cache service remains reachable. Browser automation uses headless Chromium; live browser viewing and manual browser takeover are retired.
### Inner Docker networks
On Docker and Kubernetes, automatic selection checks IPv4 routes and gateways from all routing tables, interface addresses and prefixes, DNS servers, and the resolved addresses of proxy and gateway hosts configured in the container environment at startup. Hosts supplied later during an agent turn are outside that initial observation. It also accounts for a Docker organization bridge that will attach later. The runtime prefers a free `172.31.0.0/16`, then tries other private `/16` ranges. The first `/24` serves `docker0`; inner Compose networks use `/24` blocks from that same pool. In automatic mode, failed observations or exhausted private space prevent session startup.
A Pod cannot discover the cluster’s complete Pod, Service and VPC CIDRs from its own network namespace. Set `SANDBOX_DIND_INNER_POOL` to a private `/16` you have checked against all those networks when deploying DinD on Kubernetes. An explicit pool still fails on every discovered overlap or invalid value. If some observations are unavailable, the runtime names them in a warning and can continue with the explicit pool; responsibility for the unseen address space remains with the operator.
After changing this pool, restart the spawner and recreate existing sessions to apply it. Restarting a runner container inside the same Kubernetes Pod retains that Pod’s environment and inner Docker store.
The egress proxy allows upstream DNS queries to the validated nameserver IPs in its `/etc/resolv.conf`, including private cluster DNS. Each exception covers only that exact IP and UDP/TCP destination port 53. Other private destinations and forwarding between attached networks stay blocked.
### IPv6 forwarding protection
Keep `sandbox`, `sandbox-egress` and `SANDBOX_RUNTIME_IMAGE` on the same release when upgrading. Before attaching an organization’s Docker build network, the spawner verifies the session’s forwarding protection. Compose and generated Docker session containers disable IPv6 with `net.ipv6.conf.all.disable_ipv6=1` and `net.ipv6.conf.default.disable_ipv6=1`; preserve both in custom Docker definitions.
Kubernetes Pods do not receive unsafe sysctls automatically. The egress proxy needs working IPv6 firewall support or IPv6 disabled in its network namespace. If the IPv6 firewall is unavailable, the entrypoint attempts that local disable and verifies the default and every interface. A read-only `/proc/sys` or denied write can prevent it; enabled IPv6 without protection still stops startup. Configure the egress Pod according to the cluster’s permitted networking settings before deployment.
## Sandbox devices
Organizations can run their sandboxes on their own machines, which they connect under [Settings > Sandboxes](https://docs.tale.dev/platform/admin/sandbox-devices). A device connects out over HTTPS to `/sandbox/tunnel` and keeps one WebSocket open. The bundled proxy forwards that path, and only that path, to the spawner's device hub; the spawner's signed API stays on the internal network. Devices need the Docker backend: the hub is off with Kubernetes.
| Name | Default | Description |
| --- | --- | --- |
| `SANDBOX_HUB_PORT` | `8004` | **Read by the spawner.** Port of the device hub, which answers only a ticket-authenticated WebSocket upgrade and a health check. `0` turns devices off: **Add device** is then unavailable. If you change the port, point `SANDBOX_HUB_UPSTREAM` at it too. |
| `SANDBOX_DEVICE_TUNNEL_URL` | `/sandbox/tunnel` as `wss://` | **Optional, read by the backend.** Where devices connect, when a proxy in front of Tale publishes the hub under another host or path. An `https://` address is used as `wss://`. |
| `SANDBOX_DEVICE_IMAGE_REGISTRY` | `GHCR_REGISTRY`, else `ghcr.io/tale-project/tale` | **Optional, read by the backend.** Where devices pull the sandbox images of the server's release. |
| `SANDBOX_HUB_UPSTREAM` | `sandbox:8004` | **Optional, read by the proxy.** Where the proxy forwards `/sandbox/tunnel`, when the spawner is not the `sandbox` service. |
A device always runs the server's release. It learns the release each time it renews its connection and replaces its own containers when the server moves on, unless it was connected with `--no-auto-update`. It keeps no data on the server: its workspaces stay on the machine. Sandboxes on a device call the backend's sandbox endpoints and the model gateway through the device's connection, along the same paths sessions use on the server; the gateway's management API is never reachable that way. A device keeps the backend and gateway addresses it received when it joined: after changing `SANDBOX_HTTP_API_BASE_URL` or `EXTERNAL_AGENT_GATEWAY_URL`, run `tale sandbox update` on each device.
A device must trust the site's TLS certificate. A deployment with `TLS_MODE=selfsigned` cannot take devices.
With a proxy of your own instead of the bundled one, forward `/sandbox/tunnel` to port `SANDBOX_HUB_PORT` of the spawner, keep WebSocket upgrades and the `Authorization` header intact, and allow connections that stay open for hours.
## Sandbox agent turns
| Name | Default | Description |
| -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TALE_EXTERNAL_TURN_DEADLINE_MS` | `1800000` (30 min) | **Optional.** How long an in-sandbox coding-agent turn (Claude Code, OpenCode, Codex) may sit with nobody draining its output before the sandbox daemon reaps it. A sliding window, re-armed every time the platform re-attaches to the output — not an absolute cap on the turn. Milliseconds. |
| `SANDBOX_LLM_GATEWAY_STREAM_IDLE_TIMEOUT_SECONDS` | `600` (10 min) | **Optional.** How long the sandbox model gateway waits for the next byte from a silent upstream model before it aborts the stream, including while a slow local model is still processing a long prompt. The backend reads it and configures the gateway. Claude Code and Codex turns wait at least as long before they give up on a silent stream and send the request again, so you can raise it for a slow local model without either of them sending a turn twice. A stalled upstream model then also holds a turn longer before the gateway aborts it. A value above 600 also raises the gateway's per-request timeout to match: it bounds a whole non-streaming answer, which an agent falls back to when a stream breaks, and how long an upstream may take to begin a streamed answer. Seconds. |
Investigate why output consumption stopped before increasing this deadline. It limits orphaned output streams, not total task duration. Recreate the consuming backend roles after changing the environment.
## Video-link ingestion (yt-dlp)
The worker uses these values for video transcript retrieval. Its image includes yt-dlp and a PO-token plugin. Use [Video ingestion](https://docs.tale.dev/self-hosted/configuration/video-ingestion) to distinguish source restrictions, egress problems, and authorized session handling. Recreate the worker after changing deployment environment values; rereading a variable inside a process does not reload `.env`.
| Name | Default | Description |
| -------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VIDEO_INGEST_PROXY_URL` | unset | Proxy for yt-dlp requests. Supported schemes: `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`; the last resolves destination DNS at the proxy. Use an approved egress service. |
| `VIDEO_INGEST_POT_PROVIDER_URL` | `http://bgutil-provider:4416` (baked) | PO-token provider URL. Defaults to the bundled sidecar when the image plugin is present. Tokens can help retrieval but do not grant access to private content or guarantee success. |
| `VIDEO_INGEST_FETCH_POT` | `always` when a provider is wired | When to request provider tokens: `never`, `auto`, or `always`. The bundled provider path defaults to `always`. Use `never` only when deliberately disabling that token path. |
| `VIDEO_INGEST_YTDLP_PLUGIN_DIRS` | `/opt/yt-dlp/plugins` (baked) | Directory yt-dlp loads plugins from — each plugin nested one level down (`//yt_dlp_plugins/…`). Defaults to the baked-in bgutil plugin dir when present; override only to add your own plugins. |
| `VIDEO_INGEST_COOKIES_FILE` | unset | Path inside the worker to a Netscape cookie file. Protect it as account credentials and use only an authorized session. The organization-scoped browser-session pool in the video guide offers managed import and revocation. |
| `VIDEO_INGEST_PLAYER_CLIENT` | `default,tv_simply` | Comma-separated YouTube player-client fallback list. When a PO-token provider is wired the default widens to `default,mweb,tv_simply` (mweb needs a GVS token); set explicitly to force a list. |
| `VIDEO_INGEST_PO_TOKEN` | unset | Manually pinned PO token (`CLIENT.CONTEXT+TOKEN`). Mainly for testing — tokens are video-ID-bound and short-lived; prefer the provider. |
| `VIDEO_INGEST_IMPERSONATE` | unset | Browser TLS/JA3 impersonation target (e.g. `safari`). Requires `curl_cffi` in the image; leave unset unless you know it's available. |
| `VIDEO_INGEST_BIN_DIR` | unset | Directory prepended to the yt-dlp/ffmpeg child's `PATH` so a self-provisioned `yt-dlp` (and its Deno runtime) installed outside the image's pinned bin dirs is found first. The backend image bakes yt-dlp into `PATH`, so leave it unset there; set it on a host or dev box running its own toolchain. |
| `VIDEO_INGEST_FFMPEG_LOCATION` | `/usr/bin/ffmpeg` | Absolute path to the ffmpeg yt-dlp uses for post-processing (subtitle conversion, audio extraction). Override when ffmpeg lives elsewhere — e.g. Homebrew's `/opt/homebrew/bin/ffmpeg` on a macOS dev box. |
# Observability configuration
Source: https://docs.tale.dev/self-hosted/configuration/observability-config
Start with container logs and dependency health. Add Prometheus metrics for trends and alerts, and configure error reporting if you need a searchable history of failures. Tale does not send these signals to an external monitoring service unless you configure one.
## Read the application logs
Containers write logs to stdout and stderr. The shipped Compose configuration uses Docker’s `json-file` logging driver with rotation at 10 MB per file and three files per container. Use the service names from your deployment’s Compose file:
```bash
docker compose logs --tail=100 backend-api backend-worker
docker compose logs -f backend-api
```
Stop following with `Ctrl-C`; the containers keep running. Look at `backend-api` for requests, authentication, and interactive chat generation; use `backend-worker` for background jobs, queued agent turns, and ingestion. Use `docker compose ps` to find a container that is restarting.
`journalctl -u docker` shows the Docker daemon’s journal. With the default `json-file` driver, it does not replace container logs. If you use journald or a log aggregator, configure the Docker logging driver and collection separately. Tale does not include a log shipper. Changing a logging driver requires recreating the affected containers.
## Enable authenticated metrics {#metrics}
Set a strong `METRICS_BEARER_TOKEN` in the deployment environment and apply the change by recreating the affected services through your deployment workflow. A simple container restart does not reload Compose environment values. Store the same token in your monitoring system’s secret store.
The proxy requires `Authorization: Bearer ` for these routes. Without a configured token, requests receive **401**.
| Route | Content | How to use it |
| --- | --- | --- |
| `/metrics/platform` | Web-tier process metrics and response-time targets | Prometheus scrape target |
| `/metrics/backend` | Backend HTTP and process metrics, queue depth, active generations and drain state | Prometheus scrape target |
| `/metrics/sla-rules` | Generated recording and alerting rules in YAML | Load as a Prometheus rules file |
The backend serves knowledge requests and runs ingestion jobs. Its HTTP and queue metrics help detect failures and backlog; they are not dedicated measurements of retrieval or generation latency. `BACKEND_UPSTREAM` selects the backend target for a split deployment. It does not create a separate knowledge metrics service.
Use a separate scrape job for each metrics route. In this example, `/run/secrets/tale_metrics_token` is a file inside the Prometheus container containing only the token. Create it through your secret-management system and grant Prometheus read access.
```yaml
scrape_configs:
- job_name: tale-platform
scheme: https
metrics_path: /metrics/platform
authorization:
credentials_file: /run/secrets/tale_metrics_token
static_configs:
- targets: ['tale.example.com']
- job_name: tale-backend
scheme: https
metrics_path: /metrics/backend
authorization:
credentials_file: /run/secrets/tale_metrics_token
static_configs:
- targets: ['tale.example.com']
```
Do not scrape `/metrics/sla-rules` as metrics. The generated rules reference latency series that need additional instrumentation; loading the file alone does not measure response-time compliance. Review them before loading through Prometheus’s rule configuration; [Prometheus and Grafana](https://docs.tale.dev/self-hosted/operate/observability/prometheus-grafana) covers the full setup.
## Choose where errors go
`SENTRY_DSN` enables optional error reporting. It can point to Sentry or a compatible service such as GlitchTip or Bugsink. Browser, backend and sandbox use the DSN and `SENTRY_ENVIRONMENT` reporting label; server events carry the process role and release version.
```bash
SENTRY_DSN=https://your-key@your-sentry-host/project-id
SENTRY_ENVIRONMENT=example-pr
SENTRY_TRACES_SAMPLE_RATE=0.1
```
Managed deployments default the reporting label to their retained `name`. To select another label, declare `"environment": { "SENTRY_ENVIRONMENT": { "env": "TALE_REPORTING_ENVIRONMENT" } }` in the deployment specification and supply the variable at the destination. The value starts with a lowercase letter or digit and contains 1–64 lowercase letters, digits or hyphens. Deployment identity, state and stored credentials stay at their existing paths.
Sandbox errors include HTTP and background-task failures. Expected client aborts and closed SSE streams after cancellation are excluded. Sandbox reporting sends errors only; browser and backend tracing settings do not enable sandbox tracing. It uses the backend’s request privacy filter described below.
`SENTRY_TRACES_SAMPLE_RATE` applies to browser performance traces; their default is 1.0 in development. Backend tracing is disabled by default. To enable it, set `BACKEND_SENTRY_TRACES_SAMPLE_RATE` between `0` and `1` (for example, `0.05` samples 5% of operations) and use a destination that accepts Sentry transactions. Recreate the backend API and worker services after changing the setting. Choose rates that fit your monitoring budget; setting the backend rate to `0` disables spans while keeping error reporting.
Backend spans measure HTTP handler completion and worker job execution. A job includes child spans for its drain check, handler or handover. Health and metrics probes are excluded; HTTP spans do not measure the lifetime of a streamed response body. Span names use route classes and registered queue names. Spans omit request and job payloads, raw SQL, identifiers, URLs, user context and breadcrumbs. Outgoing requests carry no trace headers. Agent work also has child spans for acquire, gateway provisioning, staging, execution, persistence and harvest; these use a fixed vocabulary and carry no prompts, file paths or credentials.
Backend events never include request cookies or bodies. Authorization, cookie, API-key, token, secret and session headers, webhook and share tokens in URLs, and credential query values such as OAuth codes are replaced with `[Filtered]` before sending. Stack frames and error messages are sent as they are, so choose the destination according to your data-handling requirements.
A database restart is not reported as an error; a managed upgrade recreates the database on every release. While it is unreachable, requests answer `503 DATABASE_UNAVAILABLE` with `Retry-After`, background jobs fail and retry under their queue's policy, and open live-update streams back off and resume. Each logs a warning line instead. The job queue logs one for the whole outage, not one for every failed poll. When the database stays unreachable for more than about a minute, an API process with live-update streams open sends one warning-level event for that outage. A backend process that starts while the database is unreachable waits up to about a minute for it instead of exiting, and logs a warning line for each new attempt. If the database is still unreachable after that, the start fails and is reported as an error. So a wrong host or port in `DATABASE_URL` takes about a minute to fail the start.
The backend does not report a request its client abandoned either, for example when you close a tab or cancel an upload while its body is still arriving. The request is answered with status `499`, counted among the client errors in the request metrics, and logged as a single debug line. Any other error in a request whose client has disconnected is still reported. An app request whose JSON body is empty or cut short is answered with `400 INVALID_JSON` and is not reported.
The browser leaves out errors that do not indicate a defect in Tale:
- errors thrown inside a browser extension
- requests the page cancelled itself, for example because you left the page before it finished loading
- requests the backend refused with a status below 500, such as a missing permission or a name that is already taken
- requests that got no response at all, for example while the device was offline
- requests that fail while the platform or its database restarts: the proxy's `UPSTREAM_UNAVAILABLE` during a deployment or restart, and the platform's `503 DATABASE_UNAVAILABLE`
Other server errors, with a status of 500 or above, are still reported.
## Aggregate analytics with Umami
Aggregate traffic collection is disabled by default and enabled separately for each deployment. Set `UMAMI_URL`, `UMAMI_WEBSITE_ID` and `UMAMI_PROXY_TOKEN` as described in the [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference). Use a separate website ID for each deployment. Apply environment changes by recreating the affected production service; no image rebuild is needed. Clear the website ID and apply the change to disable collection. The Vite development server does not inject this configuration.
The browser loads the Umami tracker from its own origin at `/_a/script.js` and sends curated events to `/_a/api/send`. A configured base path prefixes these URLs. Only the website ID and proxy path enter the browser configuration; the collector origin and bearer token stay on the server.
`UMAMI_URL` must point to a collector gateway that authenticates `GET /_collect/script.js` and `POST /_collect/api/send` with that bearer token. A stock Umami dashboard URL alone does not provide this gateway contract. Caddy must overwrite `X-Analytics-Client-IP` from a trusted client address. Keep application ports private, and configure trusted proxy ranges when another proxy sits in front. The server forwards the validated IP, browser User-Agent and required Umami headers; it drops browser cookies, credentials and referrer headers.
Reports contain known public page paths or private platform route templates, referrer origins, browser language, screen size, browser/OS/device information and approximate location. The collector derives visits and location from the IP without storing the raw address. Private organization and resource IDs become placeholders. Page titles, query strings, fragments, form fields and product content are excluded. The marketing site also counts completed contact and demo submissions without their contents. There is no cross-site identity, automatic click capture or session replay. Do Not Track and Global Privacy Control disable collection.
After rollout, verify the behavior with a browser that permits analytics:
1. Open a known page, navigate to another page and confirm both pageviews in the deployment’s Umami website.
2. Inspect the request body. Private routes contain placeholders; query strings, titles and form data are absent.
3. Enable Do Not Track or Global Privacy Control and confirm that collection stops.
4. Block the collector or test with it unavailable. Normal navigation must continue to work.
## Know the limits
Tale does not currently export OpenTelemetry traces through OTLP. An OpenTelemetry Collector can collect the Prometheus metrics, but scraping metrics does not produce distributed traces. End-to-end trace export needs application instrumentation as well as a collector.
For alert thresholds and response procedures, continue with [Operations](https://docs.tale.dev/self-hosted/operate/observability/operations). For a failing service, use the symptom tables in [Troubleshooting](https://docs.tale.dev/self-hosted/operate/observability/troubleshooting).
# Providers
Source: https://docs.tale.dev/self-hosted/configuration/providers
Configure an AI provider by keeping three things distinct: the connector definition, the organization’s credentials and the model server. The connector describes the endpoint and protocol; credentials control access; the endpoint operator runs the model service.
Use this page for custom provider definitions and environment-backed secrets. For creating credentials and choosing defaults in the app, follow [AI providers](https://docs.tale.dev/platform/admin/providers).
## Local provider endpoints
A local inference server needs both a provider definition and permission for the backend to reach its host. The definition does not install the server or load a model.
1. Make the inference server reachable from every backend role that will call it. `localhost` inside a container refers to that container, not the host machine. Test name resolution, network access and any TLS certificate from the actual runtime network.
2. For a private or loopback endpoint, set `TALE_ALLOW_PRIVATE_PROVIDER_HOSTS=1` in the backend deployment environment. This permits private provider hosts across that deployment; it is not a per-provider allowlist. Cloud-metadata endpoints remain blocked. Apply the change by recreating the affected containers; restarting them retains their existing Compose environment.
3. Declare the organization’s provider in `TALE_CONFIG_DIR//providers/local-models.yml`, or through the [managed configuration workflow](https://docs.tale.dev/self-hosted/configuration/config-releases). Use the native provider schema and a name that does not collide with a shipped definition. An organization admin can also create the same file from the app: **Add credential** > **Custom provider** under **Settings > AI providers**; see [Define a custom provider](https://docs.tale.dev/platform/admin/providers#define-a-custom-provider).
For example, replace the private IP and port below with your own reachable server. HTTP is accepted only for hosts recognized as private or loopback; public endpoints require HTTPS. An internal DNS name alone does not bypass the request-time private-host check.
```yaml
name: local-models
displayName: Local models
apiFormat: openai
baseUrl: http://192.168.1.20:8000/v1
catalog:
source: models-endpoint
auth:
- method: api-key
- method: env
```
This uses an OpenAI-compatible chat API and discovers models from `/v1/models`. Check the server’s actual compatibility; model listing alone does not prove that generation, tool calls or streaming work. If the server cannot list models, use `catalog.source: none` and enter exact model identifiers in the credential’s allowlist. Organization-defined providers do not load an organization-side static model file.
Then have an organization admin add a credential through [AI providers](https://docs.tale.dev/platform/admin/providers), refresh the catalog and select a specific model for a short chat. Verify the completed request in the intended inference server’s logs. Embeddings, speech and tool traffic require their own routing review; a local chat endpoint does not make them local.
## Configure audio transcription
The organization policy lives at `TALE_CONFIG_DIR//governance/transcription-model.yml`, with policy type `transcription_model`. The [Models page](https://docs.tale.dev/platform/admin/governance/content-models) edits the same selection. An absent file or an empty object means automatic selection:
```yaml
{}
```
To pin a model, supply both fields. This example uses the shipped OpenAI Whisper model and still requires an active, usable organization credential:
```yaml
providerSlug: openai
modelId: whisper-1
```
A partial pin is invalid. An unavailable pin never falls back to another model; restore its credential or the credential’s allowed models, or explicitly return to automatic selection. A configuration read or validation failure also refuses server transcription. The policy covers audio/video file transcription, video-link audio fallback and server dictation. Browser speech recognition remains independent.
With an active default credential for OpenRouter, Tale also discovers dedicated speech-to-text models from `/models?output_modalities=transcription`. The same credential and its allowed models apply. Use the exact identifier from that catalog when pinning a model, or keep automatic selection. See [OpenRouter’s speech-to-text guide](https://openrouter.ai/docs/guides/overview/multimodal/stt) for the provider’s endpoint contract.
For a custom OpenAI-compatible endpoint, use `catalog.source: models-endpoint` and have its `/models` response declare the audio model with an exact `id` and either `type: transcription` or `architecture.output_modalities: [transcription]`. A pure transcription model may omit `context_window` or report `0`; other models still require a positive value. An audio-input chat model or a text-to-speech model is not automatically a transcription candidate.
Tale sends bearer-authenticated multipart `file` and `model` fields to `POST /audio/transcriptions`. For OpenRouter it requests `response_format: json`, because some of its models reject `verbose_json`. Other compatible endpoints must accept `response_format: verbose_json`. The JSON response provides the transcript as `text`. Tale prefers a valid `duration` and falls back to valid `usage.seconds`. When neither is usable, it uses a local duration measurement where available. Timestamped `segments` can supply video timestamps; without them, the transcript remains plain text. Listing the model does not prove this API works. Refresh the catalog, select the model, then test a short recording and verify the request in that endpoint’s logs.
## Configure image generation
The organization policy lives at `TALE_CONFIG_DIR//governance/image-generation.yml`, with policy type `image_generation`. The [Models page](https://docs.tale.dev/platform/admin/governance/content-models#let-agents-generate-images) edits the same settings. A new organization is seeded with image generation off, and an absent file means off too:
```yaml
enabled: false
```
Set `enabled: true` for automatic selection, or pin a model with both fields:
```yaml
enabled: true
providerSlug: openai
modelId: gpt-image-1
```
A partial pin or an unknown field is invalid. An invalid or unreadable file leaves image generation off, and the Models page names the problem. Automatic selection tries `google/gemini-2.5-flash-image`, `gpt-image-1-mini`, `gpt-image-1` and `black-forest-labs/flux.2-pro`, in that order, through an OpenRouter or OpenAI default credential. An unavailable pin never falls back to another model. A pin may stay in the file while `enabled` is `false`.
The backend calls the image model with the organization's credential; no key enters the sandbox. For OpenRouter it sends `POST /images`, OpenRouter's Image API, with an aspect ratio of `1:1`, `3:2` or `2:3`, and records the cost the response reports. For any other provider it speaks OpenAI's images API: `POST /images/generations`, or `POST /images/edits` as multipart form data when the agent passes reference images, asking a GPT image model for `medium` quality. It then prices the token counts the response reports from the model's catalog entry. Tale discovers OpenRouter's image models from `/models?output_modalities=image`, and the shipped OpenAI catalog lists `gpt-image-1` and `gpt-image-1-mini`.
A custom OpenAI-compatible endpoint's `/models` response must declare an image model with `image` among `architecture.output_modalities` or `modalities.output`, and a positive `context_length` or `context_window`. Tale saves raster images only (PNG, JPEG, WebP, GIF) and refuses a model's SVG; a reference image an agent passes must be a PNG, JPEG or WebP file of at most 4 MB. Listing a model does not prove the endpoint works: turn image generation on, pin the model, have an agent create a test image, then check the request in the endpoint's logs.
One agent turn creates at most 16 images and runs one generation at a time. Before each call, Tale holds USD 0.25 per image against the turn's allowance and the budget caps, then books the reported cost in place of the hold when the call ends. The turn's gateway key gives up the same amount from its cap, so the model and the images together stay within one allowance; if the gateway cannot lower that cap, the image is refused. A turn on a vendor subscription has no gateway allowance, so its images are measured against the deployment's default turn allowance.
## Configure the standard agent {#configure-the-standard-agent}
The organization policy lives at `TALE_CONFIG_DIR//governance/standard-agent.yml`, with policy type `standard_agent`. The [Models page](https://docs.tale.dev/platform/admin/governance/content-models#standard-agent) edits the same settings. A new organization is seeded with the standard agent on, and an absent file means on too:
```yaml
enabled: true
```
Pin the runtime, the model or both, and replace the built-in instructions:
```yaml
enabled: true
harness: claude-code
providerSlug: anthropic
modelId: claude-sonnet-5
instructions: |
Write in the house style. Deliver every document as a Word file.
```
`harness` takes the slug of a runtime a project agent can use, such as `claude-code` or `codex`; the Models page refuses one that isn't. A model pin needs both `providerSlug` and `modelId`. A partial pin, an unknown field, or instructions longer than 20,000 characters are invalid. An invalid or unreadable file stops every standard agent from starting, and the Models page names the problem; it is never read as on. An unavailable pin never falls back to another model, and neither does a pin that a person's model access denies. The policy is read whenever a run starts, so an edit applies to the next run. With `enabled: false`, the standard agents already set up stay in their projects, but none of them starts.
## Verify sandbox model access
Chat calls a provider from the backend. Coding-agent sessions use `sandbox-llm-gateway`, so a successful chat does not prove the agent path. Make the endpoint resolvable and reachable from both the backend and the gateway; each HTTPS client must trust its certificate. A hostname such as `https://models.internal/v1` still needs the private-provider opt-in when DNS resolves it to a private address. Plain HTTP remains limited to hostname forms accepted by the provider schema, such as private IP literals, `localhost` and `.local`.
When a new sandbox session starts, the backend checks the custom provider’s hostname and DNS answers before provisioning its gateway configuration. Private destinations require `TALE_ALLOW_PRIVATE_PROVIDER_HOSTS=1`; metadata destinations are refused even with it. This preflight does not pin DNS for the gateway’s later requests. Keep provider configuration and DNS under trusted operator control.
After recreating the affected backend processes, start a new sandbox session with the intended provider, model and compatible agent runtime. Use a harmless request and verify a completed reply and the matching inference-server log entry. Inspect `sandbox-llm-gateway` logs if chat works but the agent cannot reach its model. `SANDBOX_EGRESS_ALLOWLIST` controls general sandbox web access, not this separate model connection.
Coding agents also rely on the context window the catalog reports for the model: the `context_length` or `context_window` your server lists on `/v1/models`, or 128,000 tokens when the listing publishes neither, as for a provider configured with `catalog.source: none`. Make the listing report the context your server actually serves. When that window, or a lower [context limit](https://docs.tale.dev/platform/admin/governance/policies-and-limits) for the person who started the run, is below 200,000 tokens, a managed Claude Code session compacts its conversation into a summary before the prompt outgrows it. Claude Code treats any value below 100,000 tokens as 100,000, so a model that serves less can still receive longer prompts than it holds. Give Claude Code a model with at least that much context.
A managed Claude Code session on a model other than Claude also leaves out the attribution line Claude Code otherwise puts at the start of every system prompt. That line changes with each request, so a server that caches prompt prefixes would recompute the whole conversation on every turn.
## Where the connectors live
Shipped definitions live at `configs/platform/system/providers//provider.yml`. Their static catalogs live at `configs/platform/system/models//models.yml`; for example, Anthropic uses `providers/anthropic/provider.yml` and `models/anthropic/models.yml`. These files belong to the image and change with its release.
Shipped files are read-only image inputs and are replaced on upgrade. For an external provider, use the reviewed `configuration` deployment declaration described in [CLI installation](https://docs.tale.dev/self-hosted/install/cli-install#configure-the-platform). It creates an organization-owned connector under `TALE_CONFIG_DIR//providers/` through the same native schema, while credential and policy changes use native APIs. The **Custom provider** entry of **Add credential** in the app writes the same organization-owned file and keeps every saved version under `.history/`.
## What a connector declares
A definition describes protocol, endpoint, catalog, and accepted authentication methods. It contains no organization credentials. These two excerpts show the format:
```yaml anthropic.yml
name: anthropic
displayName: Anthropic
apiFormat: anthropic
baseUrl: https://api.anthropic.com
catalog:
source: static
auth:
- method: api-key
- method: env
- method: subscription-broker
constraints:
execution: sandbox
harness: claude-code
```
```yaml openrouter.yml
name: openrouter
displayName: OpenRouter
apiFormat: openai
baseUrl: https://openrouter.ai/api/v1
catalog:
source: openrouter-api
auth:
- method: api-key
- method: env
```
| Field | Meaning |
| --- | --- |
| `apiFormat` | The request format: `openai` or `anthropic`. |
| `wireDialect: openai-modern` | For OpenAI-format endpoints: use `max_completion_tokens` and omit custom temperature for reasoning models. Leave it unset for endpoints that need the classic fields. |
| `baseUrl` | A fixed endpoint shared by credentials. |
| `endpointMode: per-credential` | Use an endpoint supplied with each credential instead of `baseUrl`, as Azure OpenAI does. |
| `catalog.source` | `static`, `openrouter-api`, `models-endpoint`, or `none`. Static entries use the model catalog described above. |
| `embedding` | Whether the provider serves embeddings: `supported` when its catalog ships a curated vector width, `unsupported` when the vendor offers no embedding model, so **Settings > Data residency > Embedding model** refuses it, or `unknown`, the default, when an admin enters the model and its vector width. Declare `unsupported` only where the vendor's own documentation says so. |
| `auth` and `constraints` | Allowed credential methods and any execution requirements, such as a named sandbox harness. |
## Environment-variable key source
For **Environment variable** authentication, the credential stores a variable name; the backend reads its value from its process environment when making a request. Inject the value through your deployment secret manager. This method does not store the API key in the application database.
Only names beginning with `TALE_PROVIDER_KEY_` are accepted. The complete name may contain at most 40 characters; the suffix uses letters, digits, or underscores. The form supplies the prefix automatically.
```bash
TALE_PROVIDER_KEY_OPENROUTER=sk-or-...
TALE_PROVIDER_KEY_OPENAI_PROD=sk-...
```
The reserved prefix prevents a credential from selecting unrelated secrets such as `SOPS_AGE_KEY` or `BETTER_AUTH_SECRET`. Validation rejects an invalid name before it can be saved.
After adding or rotating the value, recreate both `backend-api` and `backend-worker` with the updated environment. A Compose restart retains old environment values. Leading and trailing whitespace is removed before use; verify a real request after the rollout.
## Connect a subscription broker
A subscription broker supplies a pool of OAuth access tokens; Tale chooses a usable account for each agent turn. The shipped integrations are Anthropic with Claude Code and OpenAI ChatGPT with Codex, for task and automation agents. Keep direct API credentials for chats and other direct model calls. An OAuth token is not a provider API key.
Use a separate endpoint for each provider. Tale AI gateway exposes `/api/tokens/anthropic` and `/api/tokens/openai`, authenticated with its API key as a bearer token. Its combined `/api/tokens` endpoint is not the right source for a single-provider credential. The backend must reach the broker under the same host policy described for provider endpoints above.
The following example is the broker credential document built by the [AI providers form](https://docs.tale.dev/platform/admin/providers#connect-a-subscription-broker), not a provider definition file. Replace the hostname with your broker and provision `TALE_TOKEN_SOURCE_AI_GATEWAY` in both backend processes with that broker's API key. The mapped property names match Tale AI gateway; adapt them for another broker.
```json
{
"endpoint": "https://broker.example.com/api/tokens/anthropic",
"httpMethod": "GET",
"auth": {
"method": "bearer",
"secretEnv": "TALE_TOKEN_SOURCE_AI_GATEWAY"
},
"responseMapping": {
"tokensPath": "$.tokens",
"tokenField": "access_token",
"statusField": "status",
"activeValue": "active",
"expiresField": "expires_at"
},
"targetEnvVar": "CLAUDE_CODE_OAUTH_TOKEN",
"selection": "round-robin"
}
```
For OpenAI, change the endpoint suffix to `/api/tokens/openai` and `targetEnvVar` to `TALE_SUBSCRIPTION_TOKEN`. Every usable OpenAI item must also contain its vendor `account_id`. Tale passes that value as `TALE_SUBSCRIPTION_ACCOUNT_ID`, alongside the token, to Codex's ChatGPT connection. Do not substitute the gateway's `id` or `CODEX_ACCESS_TOKEN` for these values. Restrict the credential's model allowlist to model IDs supported by the ChatGPT plan; the OpenAI API catalog may contain models that are unavailable to subscriptions.
For Anthropic OAuth, use `CLAUDE_CODE_OAUTH_TOKEN`. The legacy `ANTHROPIC_AUTH_TOKEN` target remains supported when explicitly configured; it uses Claude Code's generic bearer-authentication path. Tale clears competing provider credential variables before supplying the chosen token. A target variable unsupported by the selected runtime is refused.
### Account identity and quota
Alongside the mapped token, status and expiry fields, a broker may provide these standard fields on each token item. Their names are fixed and require no additional response mapping.
| Field | Role |
| --- | --- |
| `id` | Broker account identifier |
| `provider` | Provider identifier |
| `account_id` | Vendor account identifier |
| `available` | Availability for new work |
| `available_at` | Time it becomes available again |
| `hold` | Why an unavailable account is held back |
| `usage` | Usage snapshot |
Keep `id` stable when the account's access token rotates, so retries recognize the same account. It is distinct from the vendor's `account_id` required by OpenAI. If `provider` names a different provider than the credential, the item is excluded.
`available: false` excludes the account until the ISO timestamp in `available_at`. If no reset is known, omit that timestamp or use `null`; the account then stays excluded until the broker reports it available. Status and token expiry are checked separately. Tale AI gateway computes availability from a `usage` snapshot containing `checked_at` and `windows`, each with its kind, utilization and reset time.
Older brokers can omit the optional metadata. Without `id`, retry identity falls back to a hash of the token, so it cannot recognize an account after its token changes. Missing quota information leaves an account eligible; it does not establish that quota remains.
Tale AI gateway excludes an account when a fresh usage snapshot reports a global session or weekly window at 100% and its reset has not passed. A vendor's explicit limit signal (`usage.limited: true`) also makes the account unavailable, even when its displayed utilization is lower or missing. Model-specific limits do not exclude the whole account. Usage is considered stale after 15 minutes, so unknown or stale readings leave the account eligible; an exhausted window with no reset time is held only while its reading is fresh. The next token request refreshes stale usage where the vendor supports it. After the applicable reset, the account can rejoin the pool. Provider-side rejection is still possible between usage refreshes. The gateway also reports an account as unavailable until its planned token refresh, `refresh_at`, once that refresh is less than an hour away (its default hand-out floor) and another account can take the work. Such an account carries `hold: "refresh"`, while a spent quota reads `hold: "quota"`. The gateway decides that another account can take the work without seeing Tale's own rules: the cooldown after an HTTP 429 described below, and the `account_id` that OpenAI requires. When those leave no available account, Tale uses the account held for its refresh whose refresh is furthest away instead of refusing the work. A turn therefore starts on a token that is about to be revoked only when the pool has nothing better, and a pool of one account is never held back this way. A broker that sends no `hold` field keeps every unavailable account excluded.
### Selection and recovery
`random` is the form's initial selection; `first` follows broker order. `round-robin` chooses the usable account selected least recently, with selection history stored per organization and credential. Concurrent requests from different backend processes update that history atomically; response reordering and backend restarts do not reset it. Preserving the account's history across token refreshes requires a stable `id`. These strategies distribute account selections, not tokens or active-agent capacity. Existing credentials retain their saved strategy.
When an account returns HTTP 429, Tale excludes it from new selections for this organization and credential for 60 seconds. Retries prefer accounts not yet tried during that run's failure streak. If every otherwise usable account was tried, retries may reuse one; quota and cooldown exclusions still apply. When every account is cooling down, an automatic task or automation retry is queued at once but starts only when the first account is available again. That wait uses no automatic retry when the refused run was itself retrying a failure on an HTTP 429.
An HTTP 401 during a turn served by a broker token can mean the token rotated while the agent worked. Tale requests credentials from the broker again and resumes the conversation when its handle and sandbox session remain available; otherwise it starts a fresh turn. The first two consecutive interruptions of this kind do not spend an automatic retry or exclude the account, so a replacement token on the same account can be used. A third counts like any other failure. This bound also handles a 401 caused by invalid authorization rather than rotation. An interruption after at least fifteen minutes of work starts the count again.
Unless overridden, pool requests time out after 10 seconds and accept at most 262,144 bytes. A mapped token expiry must be further away than the expiry safety margin, `expirySkewMs`, which defaults to five minutes. Keep the status and expiry mappings when using Tale AI gateway so inactive or nearly expired tokens are skipped; its own hand-out floor already holds back accounts that are about to be refreshed. The gateway's `refresh_at` field is the moment its refresh ends a token, earlier than the vendor's `expires_at`. Mapping `refresh_at` as the expiry and raising `expirySkewMs` (at most 3,600,000 ms) makes that rule strict: no turn starts on a token with less than the margin left, but while every account is that close to its refresh, the pool refuses new work, and a pool of one account does so before every refresh. Expiry values may be ISO timestamps or Unix timestamps in seconds or milliseconds.
The minimum remaining lifetime protects the start of a turn, not its full length. Long task or automation runs can outlast a token and need the bounded recovery described above. A broker retry does not guarantee that the account will provide working credentials.
If the pool has no usable account, inspect broker authorization, account status, token expiry and planned refreshes, quota resets and the response mapping. Renew the account authorization, or wait for the quota reset or the token refresh, as appropriate. Then verify a completed task or automation reply with the intended provider and runtime. A successful broker fetch alone does not test the vendor connection.
## Broker secrets from the environment
A **Subscription broker** credential can read its broker secret from the deployment environment. Use the separate `TALE_TOKEN_SOURCE_` prefix in **Secret from environment variable** and leave **Broker secret** empty. Names outside that namespace are rejected. If you supply both, the stored broker secret takes precedence. Recreate the consuming processes when rotating an environment-backed value.
When the replacement configuration still uses broker authentication, leaving both secret fields empty preserves its existing stored secret. Entering an environment reference without a new broker secret switches to the environment source. Choosing **None** for broker authentication removes the stored secret from the replacement configuration.
## Keep organization settings with the organization
Credential names, allowed models, defaults, and enabled state remain organization data, normally managed under [AI providers](https://docs.tale.dev/platform/admin/providers). A managed configuration release can create exact environment-backed credentials through native APIs after verifying the organization and operator. It does not install an inference server or prove model behavior; complete the local endpoint checks above after deployment.
# Configure retention bounds
Source: https://docs.tale.dev/self-hosted/configuration/retention
Retention controls how long Tale keeps each category of data. Operators set the allowed bounds; organization admins enable categories and choose a duration within those bounds. A shorter duration can delete existing history, so review its effect before applying it.
## Understand bounds and policy
Two files under `TALE_CONFIG_DIR//governance/` have different jobs:
| File | Purpose |
| --- | --- |
| `retention.yml` | Operator bounds and defaults for every category. JSON is also accepted. |
| `retention-policy.yml` | The organization’s chosen enabled states and durations. Managed by the governance settings. |
The organization gets its own files when it is created. A change to one organization’s file does not change another organization’s policy. There is no fallback to a `default` organization when an organization’s bounds file is missing.
Each category has `min`, `max`, `default` and `unit`. Raising `min` requires data to be kept longer; lowering `max` limits how long it may be kept. Neither value enables cleanup by itself. The applied policy determines whether the category is enabled.
## Edit the organization’s bounds
Start from the organization’s existing complete file. Preserve categories you are not changing. This fragment shows the shape of one category, rather than a replacement for the whole file:
```yaml
chatHistory:
min: 30
max: 730
default: 90
unit: days
```
Most categories use days; `userTempHours` and `agentTempHours` use hours. The token-usage category is named `usageLedger`. Use the category identifiers already present in the file so validation can catch mistakes.
Environment overrides are declared explicitly by the file’s root `_metadata.envNames`, with an optional `_metadata.envPrefix`. The bundled file maps `TALE_RETENTION_AUDIT_MIN` to `auditLog.min`, for example. A minimum override can only raise the floor; a maximum override can only lower the ceiling. Restart the backend processes after changing their environment.
Two floors are fixed: `auditLog.min` cannot be lower than 180 days, and `loginAttempt.min` cannot be lower than 90 days. Tale does not load a file that undercuts either. Until you correct it, the organization’s **Retention policy** shows no allowed ranges and cannot be saved, and the backend log names both floors. Scheduled cleanup keeps using the bounds applied last.
The bundled file lets an organization keep audit logs for 180 to 3650 days. Upgrading Tale does not rewrite an organization’s file, so an organization created by an earlier release can still have `auditLog.min: 365`. Lower that value to allow a shorter audit retention, or raise it when a regulation or contract requires a longer audit history.
## Review and apply a change
After editing bounds, ask the organization admin to review the proposed change in [Policies and limits](https://docs.tale.dev/platform/admin/governance/policies-and-limits). Cleanup uses the applied bounds snapshot; an operator’s file edit alone does not silently apply new bounds.
Review enabled categories, the old and new durations, and any grace period before applying. A value such as `auditLogRetentionDays: 730` is a chosen duration, while `auditLog.min: 180` is a lower bound. Keep those meanings separate when reviewing a diff.
Test a shorter policy on synthetic data first. Confirm that a record inside the window remains, an expired record follows its category’s deletion behavior, and a held record remains protected.
## Understand the cleanup result
The backend worker runs scheduled cleanup per organization. Threads, documents, contacts and external conversations have lifecycle handling; row-level categories can be deleted directly after their applicable retention and grace period. Do not assume that every deleted record appears in Trash.
Each run deletes a limited number of records per category and organization: up to 50,000 chat filter events, and batches of up to 1,000 records for every other category. A larger backlog, such as a long history when a category is first enabled, takes several daily runs to clear.
Audit retention is also organization-scoped. It removes the oldest eligible prefix of that organization’s audit chain, stopping when a held row must remain. One tenant’s shorter window does not shorten another tenant’s history.
Each run is recorded in the organization’s [audit log](https://docs.tale.dev/platform/admin/governance/audit-logs) as system events in the data category. It begins with **Retention run started**, adds one event for each category that deleted records, carrying the number removed rather than one event per record, and ends with **Retention run completed**. A run that stops on an error, or keeps due records because their deletion failed, ends with **Retention run failed** instead; the next scheduled run retries those records. All events of one run name the same retention run as their target, and a run that finds nothing to delete still records its start and end.
`TALE_RETENTION_DISABLED=true` pauses scheduled retention cleanup for an operator-controlled maintenance window. It does not restore deleted data or disable other deletion paths. Record when you enable it and remove it when the maintenance is complete.
## Preserve held data
Legal holds override retention for their supported scope. An organization-wide hold protects the organization; narrower holds protect the matching entities or custodians. Review the [legal hold workflow](https://docs.tale.dev/platform/admin/governance/legal-hold) before changing a policy that affects held data.
A hold is not a backup. Once deletion has completed outside a hold, increasing the retention duration cannot recover the data; recovery depends on a retained backup and its matching deployment state.
# Protect configuration secrets with SOPS
Source: https://docs.tale.dev/self-hosted/configuration/secrets-with-sops
Tale uses SOPS and age for supported configuration secret sidecars, including knowledge-database and object-storage connection files. Current AI-provider credentials are stored separately in the application database and use `ENCRYPTION_SECRET_HEX`. Rotating an age key does not rotate those database credentials.
## Identify the secret you are changing
Use the storage mechanism to choose the correct key:
| Secret storage | Encryption control | Operational consequence |
| --- | --- | --- |
| SOPS-enabled `*.secrets.json` configuration sidecar | `SOPS_AGE_KEY` or `SOPS_AGE_KEY_FILE` | Preserve a key that can decrypt every retained file and backup. |
| Database provider credentials and other secret-box values | `ENCRYPTION_SECRET_HEX` | Replacing the key makes existing ciphertext unreadable; age rotation does not migrate it. |
| Provider credential backed by an environment variable | `TALE_PROVIDER_KEY_*` | Rotate through the deployment’s secret manager and restart the consumers. |
A retired `providers/.secrets.json` file may still exist in old configuration trees. Its presence does not mean that current provider credentials use that file. See [Providers](https://docs.tale.dev/self-hosted/configuration/providers) for the current credential model.
## Choose an age-key source
An inline `SOPS_AGE_KEY` takes precedence over `SOPS_AGE_KEY_FILE`. Use one source deliberately. The file form accepts one private age key per line and ignores blank lines and `#` comments; all configured recipients are included when Tale writes new SOPS ciphertext.
The file path is resolved inside the process that reads it. A host path in `.env` is insufficient by itself: mount the key file into each container that needs it, set the in-container path and restrict filesystem access. Recreate the consuming containers after changing their environment; `docker compose restart` does not load changed environment definitions.
If both variables are unset, the SOPS helper writes supported sidecars as plaintext JSON with mode `0600`. It still recognizes existing encrypted files and refuses to read them without a key. Unsetting the variables does not decrypt existing files.
## Prepare a rotation
Inventory the SOPS-encrypted files and their backups before replacing a key. Keep a protected copy of the old key and verify that you can decrypt a representative file without printing its contents or sending them to logs.
Create a new age key through your existing secret-management tooling. Build a protected key file containing **both the existing private key and the new private key**. Do not overwrite the old key file with a command that generates only the new key.
Update the deployment to mount that file and use `SOPS_AGE_KEY_FILE`. Remove the inline value from the consuming environment, or it will continue to take precedence. Roll out the environment change and confirm that existing connections still work.
## Re-encrypt and verify
Rewrite each affected sidecar through its supported configuration save path or an operator-controlled SOPS re-encryption procedure. Tale addresses newly encrypted files to all currently configured recipients; merely adding a key leaves existing ciphertext unchanged.
Do not remove the old key until every active encrypted file has been checked with the new key alone. Keep the old key protected for historical backups that still require it.
After that verification, deploy a key file containing only the new key. Restart the consuming processes to clear decrypted caches, then test each affected connection. A successful process restart alone does not prove that every file can be decrypted.
## Recover a decryption failure
| Symptom | Check |
| --- | --- |
| Encrypted file found without a key | Restore the matching key source; disabling encryption does not convert the file. |
| Key-file read fails | Check the mount, in-container path, ownership and permissions. |
| Old key is still selected | Remove the non-empty inline `SOPS_AGE_KEY` before relying on the file. |
| New key cannot decrypt one file | Keep the old key and re-encrypt that file before completing the rotation. |
| Provider credential fails after changing `ENCRYPTION_SECRET_HEX` | Follow the database-secret recovery path; changing age keys cannot repair it. |
For secrets managed by Vault, Kubernetes or another external store, prefer a provider’s [environment-variable key source](https://docs.tale.dev/self-hosted/configuration/providers#environment-variable-key-source) where supported. Keep encryption keys with your recovery plan, separately protected from the data backups they unlock.
# TLS and domains
Source: https://docs.tale.dev/self-hosted/configuration/tls-and-domains
Choose the URL users will open and the service that terminates TLS before configuring sign-in or inviting people. Tale's Caddy proxy can use its internal certificate authority, obtain public certificates, or serve HTTP behind your own TLS proxy.
## Choose a TLS mode
| `TLS_MODE` | Use it when | What you operate |
| --- | --- | --- |
| `selfsigned` | Developing locally or using a private environment whose clients trust your CA. | Install Caddy's root certificate in each client trust store. |
| `letsencrypt` | Serving a public hostname through Tale's proxy. | Public DNS, reachable ports 80/443, and persistent Caddy certificate storage. |
| `external` | A load balancer or reverse proxy already handles TLS. | The upstream certificate, trusted forwarding, and the private HTTP connection to Tale. |
Keep `SITE_URL` as the public URL and `HOST` as its hostname. Applying new environment values requires recreating the affected services. With the workspace CLI, use the deployment workflow and include `--stop` when the proxy must be recreated; inspect the preview and allow for downtime. With your own Compose file, the service is `proxy`, not the generated container name.
## Trust a private development certificate
`TLS_MODE=selfsigned` makes Caddy issue certificates from its internal CA. A browser warning means that client does not trust the issuing CA or the hostname does not match; check both.
Copy the **public root certificate** from the running proxy container. Set `TALE_PROXY_CONTAINER` to the actual container name from your deployment:
```bash
docker cp "$TALE_PROXY_CONTAINER:/data/caddy/pki/authorities/local/root.crt" ./tale-local-root.crt
```
Verify that the certificate came from your own instance, then install it using the operating system's or browser's trusted-certificate settings on each client that needs access. Do not distribute the CA's private key. Running `caddy trust` through `docker exec` affects the container's trust store, not your workstation's. See [Caddy's local HTTPS guidance](https://caddyserver.com/docs/automatic-https#local-https) for the trust boundary.
## Obtain a public certificate
1. Point the hostname's public DNS records at the intended host. Check both A and AAAA records when IPv6 is configured.
2. Make ports 80 and 443 reachable at that proxy, and preserve its `caddy-data` volume across replacements.
3. Configure the public URL and certificate mode:
```bash
HOST=tale.example.com
SITE_URL=https://tale.example.com
TLS_MODE=letsencrypt
TLS_EMAIL=ops@example.com
```
4. Apply the configuration, inspect `tale logs proxy`, and open the public URL from another machine. Check the hostname and certificate chain in the browser.
Caddy handles issuance and renewal. DNS, firewall, ACME, and storage problems can delay or prevent them, so monitor certificate expiry and proxy errors rather than assuming a fixed issuance time. `TLS_EMAIL` provides the ACME contact address; it is not a substitute for expiry monitoring. [Caddy's automatic HTTPS requirements](https://caddyserver.com/docs/automatic-https) describe the public network prerequisites.
## Use an upstream TLS proxy or custom certificate
Set `TLS_MODE=external` when another proxy terminates TLS, while keeping the browser-facing HTTPS URL:
```bash
HOST=tale.example.com
SITE_URL=https://tale.example.com
TLS_MODE=external
TRUSTED_PROXIES=10.20.0.0/16
```
Tale's Caddy instance serves HTTP inside this arrangement, so your proxy has to report how the browser connected. Forward the original `Host` header and send `X-Forwarded-Proto: https`; Tale uses both to keep each browser on the origin it opened. Caddy accepts forwarded headers only from the addresses in `TRUSTED_PROXIES`: CIDR ranges separated by spaces, or `private_ranges` for every private and loopback address, which is the default when the variable is unset. Set it to the range your proxy connects from. The proxy refuses to start on any other value, and the other TLS modes ignore the variable.
Keep the HTTP hop private. The proxy container publishes port 80, so allow only your TLS proxy to reach it: a client inside a trusted range could otherwise claim an HTTPS connection it never made. Verify sign-in callbacks, secure cookies, uploads, and streaming through the complete path. A custom certificate installed on the upstream proxy is independent of Tale's TLS mode.
If you maintain a custom Tale proxy image and Caddyfile instead, mount your certificate and private key read-only and configure Caddy's `tls ` directive yourself. Merely mounting the files or setting `TLS_MODE=external` does not make Caddy load them. The custom configuration must retain Tale's routes, health behavior, and metrics protection.
## Change the public domain or base path
Update `HOST` and `SITE_URL` together, plus any browser-facing storage endpoint and identity-provider callback registrations that use the old origin. Recreate the affected application and proxy services, then test sign-in, an existing file download, an upload, and a live-updating page at the new URL.
For a managed deployment created with `identity.bootstrap: "fresh"`, follow the [managed hostname migration procedure](https://docs.tale.dev/self-hosted/install/cli-install#managed-origin-migration) as part of this transition. It requires the retained deployment state and an explicit `identity.migrateOriginFrom`; changing only `HOST` and `SITE_URL` does not update the managed identity and client journals. Keep the existing account, organization and client credentials, then export the consumer configuration for the new issuer after the deployment is ready.
For a subpath such as `https://example.com/app`, also set `BASE_PATH=/app`. Keep that prefix on requests sent to Tale's proxy: its generated routing strips the prefix internally. Check absolute links and callbacks rather than verifying only the home page. Keep the old domain available during a planned transition if users still need its existing links or sessions.
## Serve several domains {#several-domains-at-once}
List additional bare origins in `ADDITIONAL_SITE_URLS`, separated by commas or whitespace:
```bash
HOST=tale.example.com
SITE_URL=https://tale.example.com
ADDITIONAL_SITE_URLS=https://tale.partner.example,https://app.example.org
```
An origin has a scheme, host, and optional port, but no path. Caddy serves the listed origins and requests their public certificates in `letsencrypt` mode. Configure DNS and reachability for each. These origins are separate entry points, with cookies scoped to the domain where the user signs in.
Tale accepts only configured origins when deriving browser-facing URLs; an unrecognized host falls back to `SITE_URL`. Do not use that fallback as a domain-configuration shortcut.
In a managed deployment, declare these origins as `additionalOrigins` in the deployment specification instead of editing the runtime environment; see [Serve additional origins](https://docs.tale.dev/self-hosted/install/cli-install#managed-additional-origins). The CLI manages `ADDITIONAL_SITE_URLS` there and keeps the native identity on the primary origin.
### Keep canonical settings stable
| Setting | Why the canonical domain matters |
| --- | --- |
| E-mail links and notifications | Background work has no browser origin to use. |
| SAML SP entity ID | The identity provider identifies one stable service provider. |
| SCIM resource locations | Directory synchronization needs stable resource URLs. |
| Passkeys | Credentials are bound to a relying-party domain and do not transfer automatically between domains. |
| Public object-storage endpoint | Background work signs file links for this endpoint. A link handed to a browser uses the domain the browser is on when the endpoint is one of this deployment's origins; review a separate file host when changing domains. |
### Register every provider callback
Open **Settings > Enterprise SSO** to copy each domain's OIDC redirect or SAML ACS URL. SAML metadata includes the configured ACS entries. For connector consent, use the per-domain redirect URLs under **Settings > Connectors > OAuth apps**. Register the required URLs with each provider and test a fresh sign-in from every supported origin.
# Configure video transcript ingestion
Source: https://docs.tale.dev/self-hosted/configuration/video-ingestion
Tale uses `yt-dlp` to retrieve content for video-link ingestion. Availability depends on the video, supported captions or extraction path, and the source platform's access checks. A video that plays on your laptop may still reject the server's network or session.
This operator guide covers transcript retrieval configuration. Start with a public video you can access and inspect its ingestion error before adding credentials or changing egress.
## Identify the failing stage
| Observation | Check first |
| --- | --- |
| One video fails | Whether the URL is supported, the content is still available, and a usable transcript or audio path exists. |
| Many videos fail from one host | Worker extractor errors, source-platform responses, and that host's network path. |
| Retrieval works but knowledge search does not | The organization's embedding configuration and indexing status. |
| Failures begin after a session worked | Session expiry, source-account state, and cooling or retirement in the pool. |
Read `tale logs backend-worker --tail 200` and the item's failure reason. Keep the URL and error category, but remove account cookies, signed URLs, and credentials before sharing diagnostics. Retries may help a transient failure; repeated identical refusals need investigation.
## Check the built-in token provider
The platform image includes the token plugin, and the packaged deployment starts `bgutil-provider` on the internal network. The default endpoint is `http://bgutil-provider:4416`. It supplies proof-of-origin tokens used by supported extractor requests; it does not grant access to private content or guarantee that a source accepts the request.
Check `tale logs bgutil-provider` and whether the worker can reach the service. The sidecar is best-effort: its failure does not prevent the core deployment from starting, though transcript retrieval may degrade.
`VIDEO_INGEST_POT_PROVIDER_URL` selects another provider endpoint. `VIDEO_INGEST_PO_TOKEN` supplies a manually obtained token. Keep token material in your secret configuration. The [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference) also lists extractor-client and plugin options; change them only when the observed error calls for it.
## Configure an egress proxy
Use `VIDEO_INGEST_PROXY_URL` when your deployment requires video retrieval through an approved proxy. Metadata, captions, and audio requests use that configured path. Supported schemes include `http`, `https`, `socks4`, `socks4a`, `socks5`, and `socks5h`; the latter resolves DNS at the proxy.
```bash
VIDEO_INGEST_PROXY_URL=socks5h://proxy.example.com:1080
```
Add credentials through your secret-management workflow if the proxy requires them. An invalid or unsupported proxy URL is ignored with a warning, so confirm the applied configuration and a real retrieval result. Recreate the worker after changing its container environment; restarting the existing container does not import a changed `.env`.
A different route is not a promise of access. Confirm that your proxy and the source account are authorized for the content you need. If the source remains unavailable, import a transcript you already have as a [Knowledge document](https://docs.tale.dev/platform/knowledge/documents).
## Import an authorized browser session
The server can draw cookies from a session pool keyed by **organization and domain**. It encrypts cookie jars using `ENCRYPTION_SECRET_HEX` and does not return those cookies in list responses. Pool access is part of server-side video ingestion, not a cookie export to agent scripts.
There is no in-app import form. The REST write requires a key whose user is an organization administrator and is on `TALE_DEPLOYMENT_CONFIG_ADMINS`; the key must also resolve the target organization. `GET /api/v1/me` reports `capabilities.deploymentEditor` for the key. Name the organization explicitly with `X-Organization-Slug`, especially when the user belongs to several organizations.
1. Export a Netscape-format cookie jar from an authorized browser session for the source domain. Treat the file as account credentials and keep it outside source control.
2. Set `TALE_URL`, `TALE_API_KEY`, and `TALE_ORG_SLUG` for the intended instance and organization. Keep `cookies.txt` readable only by the operator's account.
3. Import the jar without placing its content in command arguments:
```bash
jq -n --arg domain youtube.com --rawfile cookiesJar cookies.txt \
'{domain: $domain, cookiesJar: $cookiesJar, label: "operator-managed session"}' |
curl --fail-with-body -sS -X POST "$TALE_URL/api/v1/browser-sessions/import" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $TALE_ORG_SLUG" \
-H 'Content-Type: application/json' \
--data-binary @-
```
A successful import returns HTTP 201 with a `sessionId`. Invalid data returns a validation refusal; a permission failure returns 403. Resolve the named gate rather than granting a broader role just to make the request pass.
## Check and retire sessions
```bash
curl --fail-with-body -sS "$TALE_URL/api/v1/browser-sessions" \
-H "Authorization: Bearer $TALE_API_KEY" \
-H "X-Organization-Slug: $TALE_ORG_SLUG"
```
The list shows metadata such as status, expiry, and failure count. The default lifetime is 14 days; an import can set positive `ttlMs` up to 180 days. Source cookies can expire earlier, so an unexpired pool record does not prove the account session still works.
Blocked retrieval cools a session; repeated blocks can retire it, and a scheduled sweep handles cooled or expired records. A later retry can use another healthy session for the same organization and domain. If no session is available, retrieval can continue without one using the other configured options.
To revoke an imported session, use `DELETE /api/v1/browser-sessions/` with the same organization scope and write permissions. Confirm the ID from the list first. Rotate or revoke the source account's session as well if its cookie jar was exposed. Finally, retry a controlled video and verify the transcript and indexing result; importing cookies alone does not prove ingestion succeeded.
# Run Tale on your infrastructure
Source: https://docs.tale.dev/self-hosted
Self-hosted Tale gives your organization control of the deployment, storage, and model connections. The open-source platform provides the same product functionality used in the enterprise edition. Your team operates the infrastructure and decides which external services it may contact.
For a first installation, follow the [local quickstart](https://docs.tale.dev/self-hosted/install/quickstart): install the published CLI, run `tale init`, then `tale dev`. The guide takes you through the owner account, provider connection and first chat reply. Docker runs the packaged services; no source checkout or build toolchain is required. If `tale --help` lists `doctor`, you can use `tale doctor` to check your machine before creating the project.
## Choose your starting point
| What you need | Start here |
| --- | --- |
| Try a local instance or install a new workspace | [Installation quickstart](https://docs.tale.dev/self-hosted/install/quickstart) |
| Understand services, data, and network connections | [Architecture overview](https://docs.tale.dev/self-hosted/overview) |
| Supply your own Compose or Kubernetes deployment | [Run your own stack](https://docs.tale.dev/self-hosted/install/own-compose) |
| Change the application source | [Contributor setup](https://docs.tale.dev/develop/contributor-setup) |
| Use an instance someone else operates | [Send your first message](https://docs.tale.dev/get-started/quickstart) |
## Plan what your team will operate
Assign responsibility for access, TLS, upgrades, backups, monitoring, and incident response before inviting users. Configure an AI provider and, when you need searchable documents, an embedding model and knowledge storage. Test an upload and a complete chat before treating the instance as ready.
Hosting the application yourself does not keep every request on the same network. A configured model provider, connector, web crawler, or external monitoring destination can receive data. Review the actual destinations in [Security hardening](https://docs.tale.dev/self-hosted/operate/security/hardening) and your provider configuration. An isolated deployment needs locally available images, models, credentials, and dependencies.
## Configure and maintain the instance
Use the [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference) for deployment variables and the configuration guides for organization settings. The [container architecture](https://docs.tale.dev/self-hosted/operate/container-architecture) explains operational dependencies; [Backups and restore](https://docs.tale.dev/self-hosted/operate/backups-and-restore) covers recovery planning.
If your team wants Tale to operate the service, read [Tale Cloud](https://docs.tale.dev/cloud). Platform guides apply to both hosting options.
# Install the tale CLI
Source: https://docs.tale.dev/self-hosted/install/cli-install
The `tale` CLI installs, deploys and operates Tale. Install it on the machine where you will run operator commands, then choose the [local quickstart](https://docs.tale.dev/self-hosted/install/quickstart) or the deployment workflow below.
The same CLI owns workspace container operations, managed deployments from exact source commits, and client configuration releases. Your deployment automation selects destination, pins and credential references, then calls the CLI. [Release client configurations](https://docs.tale.dev/self-hosted/configuration/config-releases) covers content from the client's own repository.
For your first instance, use the [quickstart](https://docs.tale.dev/self-hosted/install/quickstart). The ordinary path is `tale init` followed by `tale dev`. You only need a managed deployment specification when your automation must pin exact runtime and client source commits.
## Before you begin
You need:
- A workstation running macOS, Linux, or Windows with PowerShell.
- For local container operations: Docker with Compose and a running Docker daemon.
- For a remote workspace: access to its Docker daemon, usually through an SSH Docker context. The remote operator must be able to run Docker.
The bundled object store currently ships only a `linux/amd64` image. On an ARM64 host, local development and workspace deployment need working amd64 emulation: Docker Desktop includes it; a standalone Linux Docker host needs [QEMU registered on the host](https://docs.docker.com/build/building/multi-platform/#install-qemu-manually). Tale selects the amd64 image but does not install emulation. Managed bundles still require native images for their declared architecture, so an ARM64 managed deployment must wait for a native object-store image.
The installer downloads a release binary from GitHub. It needs access to `raw.githubusercontent.com`, `api.github.com`, `github.com` and the release download destinations that GitHub redirects to.
## Run install-cli.sh or install-cli.ps1
On macOS or Linux:
```bash
curl -fsSL https://raw.githubusercontent.com/tale-project/tale/main/scripts/install-cli.sh | bash
```
On Windows PowerShell:
```powershell
irm https://raw.githubusercontent.com/tale-project/tale/main/scripts/install-cli.ps1 | iex
```
The Unix installer selects the binary for your OS and CPU. By default, it replaces an existing `tale` executable found on `PATH`, or installs into `/usr/local/bin`; it requests `sudo` only when needed to write there. The Windows installer uses `%LOCALAPPDATA%\Programs\tale` by default and updates your user `PATH`.
Release binaries cover macOS on Apple Silicon and Intel, Linux on x86_64 and arm64, and Windows x64. Windows ARM requires x64 emulation. An unsupported Unix architecture produces a build-from-source message.
Set `VERSION` to a release version to pin the install, and `INSTALL_DIR` to choose another destination. In a Unix shell, **export** these variables before running the pipeline so the `bash` process receives them; setting them only before `curl` does not pass them to the installer. In PowerShell, use `$env:VERSION` and `$env:INSTALL_DIR`.
| OS | Installer script |
| ------- | ------------------------- |
| macOS | `scripts/install-cli.sh` |
| Linux | `scripts/install-cli.sh` |
| Windows | `scripts/install-cli.ps1` |
## Verify
```bash
tale --version
```
The CLI prints its installed version. If the command is not found, check the destination in the installer output and ensure that directory is on `PATH`. If an existing Windows terminal still cannot find `tale`, reopen it. If the download fails, check the network destinations above; an optional `GITHUB_TOKEN` environment variable authenticates the release lookup when anonymous GitHub API requests are rate limited.
## Confirm configuration
For workspace container operations, use the project created by `tale init` in the [quickstart](https://docs.tale.dev/self-hosted/install/quickstart). The CLI walks up the directory tree to find its `tale.json`; check the selected project before operating on it:
```bash
tale config show
```
Configuration releases and [managed deployments](https://docs.tale.dev/self-hosted/install/cli-install#managed-deployments) select their sources and destinations explicitly and do not align to a nearby workspace. `config show` keeps its existing local-project behavior.
For a workspace deployment, the host the proxy answers on, TLS settings, and every secret live in the project's `.env`. For production, edit `HOST` there or pass `--host` to `tale deploy`. Local `tale dev` defaults to `localhost`; its `--host` and `--port` flags apply only to that run. To operate a remote workspace host, point your shell's Docker context (or `DOCKER_HOST`) at it. Managed bundle deployment instead runs on its declared destination with the local Docker daemon.
## Run tale deploy
```bash
tale deploy
```
Without `--bundle`, `tale deploy` ships the CLI's own version: it pulls that version's images, restarts affected containers in order and runs schema migrations. Use `tale update` first to choose another workspace version. For independently pinned runtime and client source commits, follow [Managed deployments](https://docs.tale.dev/self-hosted/install/cli-install#managed-deployments).
## Command reference
The CLI groups its commands by what you are doing, the same way `tale --help` does. Each command and its arguments are listed below. How to read the notation:
- A positional argument in `[square brackets]` is **optional**; one in `` is **required**.
- Required options are named explicitly for configuration releases; other flags are optional unless command help marks them as required.
- A flag written `--flag ` **requires a value** when you use it (e.g. `--port 8443`); a bare flag like `--detach` is a boolean switch.
- **Defaults** are shown in parentheses after the description. No default means the flag is off, or the command resolves the value from `.env` / context.
This reference includes commands in the current source code. The published v0.5.70 CLI does not include `tale doctor` or `tale dev --stop`. Run `tale --help` and `tale --help` for the commands and options available in your installed version.
**Global flags** work on every command:
- `--verbose` — verbose output: debug logs and the raw subprocess stream (long form only; there is no `-v`).
- `-q, --quiet` — only warnings and errors.
- `-y, --yes` — assume "yes" for all prompts (non-interactive).
- `--no-color` — disable ANSI colour (also honours `NO_COLOR` / `FORCE_COLOR`).
- `--json`—machine-readable JSON on stdout; supported by `doctor`, `status`, `sandbox status`, every `config` subcommand and managed deployment commands.
- `--ci` — force non-interactive, append-only output (no cursor control).
Commands exit `0` on success, `2` on a usage error, `3` on an unmet precondition (no project, Docker not running, port in use), `4` on a user abort (Ctrl-C, or a required prompt with no terminal), and `5` on an external-dependency failure — so scripts can branch on the cause.
### Setup
`tale doctor` — inspect local startup prerequisites without creating a project, installing software or changing configuration. It checks the Docker daemon, Compose support and Linux-container mode, reports the daemon architecture and checks local ports. A remote Docker context skips local port checks. ARM64 and occupied-port warnings require review; an existing instance may already own a port.
- `-p, --port ` — HTTPS port to check (default `443`); the sandbox port `8003` is also checked.
- `--json` — emit the checks as machine-readable JSON.
Docker, Compose, unsupported container-mode failures or selecting HTTPS port `8003` exit `3`. Warnings alone exit `0`; this does not verify image downloads, storage capacity or a model provider. Check a different port with `tale doctor --port 8443`, then use the same port with `tale dev`.
`tale init [directory]` — create a project: it scaffolds the example configs, `AGENTS.md` + a `CLAUDE.md` pointer, and a local-default `.env` (localhost, self-signed certificate, generated secrets). No Docker is needed, and the production domain and TLS are chosen later, at `tale deploy`. In a terminal it asks for a project name when `directory` is omitted, confirms before overwriting an existing project, and asks once whether agents may run `docker` inside sandboxes (default: no — enabling it runs a privileged inner Docker); non-interactive runs skip all prompts. `directory` is optional (default: the current directory).
- `-f, --force` — overwrite an existing `tale.json` instead of aborting.
- `--no-env` — scaffold the project but skip `.env` generation.
`tale dev` — launch all services locally with a self-signed certificate. The selected host and port set the local application origin for this run, without rewriting the production settings in `.env`. Readiness waits for services with health probes to become healthy and the remaining services to be running. If startup times out, inspect the reported service logs before retrying.
- `-d, --detach` — run in the background instead of streaming logs.
- `--stop` — stop this project’s local development containers and preserve their data. Run `tale dev` again to resume.
- `-p, --port ` — HTTPS port to expose (default `443`).
- `--host ` — host alias for the proxy (default `localhost`).
- `-y, --yes` — non-interactive: auto-accept prompts (e.g. installing or starting Docker).
`tale deploy` — deploy the current CLI version with blue-green replacement of application roles. Shared execution services roll in place; database and proxy replacement needs `--stop`. On first deployment, the CLI asks for the production domain and TLS email unless supplied. Read [Upgrades](https://docs.tale.dev/self-hosted/operate/upgrades) before changing an existing installation.
- `--stop` — also update the stop-gated tier (`db`, `proxy`) — recreates those containers, so accept a brief downtime; without it, running `db`/`proxy` are left untouched.
- `-s, --services ` — update only these comma-separated services (default: all rotatable services).
- `--host ` — host alias for the proxy (default: the `HOST` value from `.env`).
- `--override` — overwrite container config from the host workspace (encrypted `*.secrets.json` and `.history/` are always preserved).
- `--override-all` — factory-reseed the builtin catalog into every org server-side; implies `--stop`.
- `-q, --quiet` — suppress container logs during the deploy.
- `-y, --yes` — auto-accept destructive confirmation prompts (e.g. `--override-all`).
- `--skip-backup` — skip the automatic pre-deploy volume snapshot.
- `--dry-run` — preview what would change without touching anything.
### Managed deployments
Use a reviewed deployment specification when the runtime and client configurations must follow exact source commits. Deployment automation selects the destination, credentials and pins and calls the Tale CLI. The CLI acquires source, resolves and verifies image digests, prepares the transfer, preserves supported existing state, takes recovery snapshots when required, rolls the stack, provisions the native instance and verifies configuration content. Keep those deployment internals in Tale.
#### Prepare the runtime and source pins
Run preparation with a compiled CLI built from a clean, committed Tale checkout on Linux, matching the destination's `linux/amd64` or `linux/arm64` architecture. That same executable is included for backend-local provisioning. Preparation needs Git and Docker for source/image verification; applying runs on the destination with its local Docker daemon, retained state directory and environment. The full CLI commit, runtime source commit and client configuration source commit are separate pins. On the destination, the prepared runtime publishes only the proxy's ports 80 and 443 and the two databases on `127.0.0.1:5432` and `127.0.0.1:5433`; the sandbox spawner (8003) and its device hub (8004) stay on the internal network, where the backend and the proxy reach them.
A managed deployment records its recovery point before it changes anything: the pre-deployment snapshot and the bundle being applied, kept in the state directory until the ready receipt is written. An interrupted deployment therefore expects the same bundle on retry and refuses a different one, naming the pending bundle's sha256. When that bundle can no longer complete — a corrected CLI is now pinned, say — declare the named sha256 as `supersedesPendingBundle` in the deployment specification and prepare again: the reviewed bundle takes over the same snapshot, the ready receipt lists it under `supersededBundles`, and the declaration comes out afterwards. That takeover covers the deployment's recovery point, not a runtime rollout in progress. A rollout that stopped after verifying its images — at Compose startup or while waiting for health checks — resumes only with the same prepared runtime (`runtime/runtime.json`) and the same origin, TLS and environment settings; a bundle whose runtime or settings differ is refused, superseding or not, with `A different runtime operation is pending; recover that exact operation first.` Finish it with the bundle that started it before you move the CLI pin in either direction: a different CLI can prepare a different runtime from the same runtime commit. The backend-local phases (`deploy provision`, `deploy export-client-native`) run inside the backend as its own user, the owner of its data directory, and when one fails the deploy result repeats the inner CLI's own summary.
The pre-deployment snapshot also holds the model gateway's store, `llm-gateway-data`, which a newer gateway migrates forward-only the first time it starts on it. A pending deployment that changed nothing records no snapshot; a `supersedesPendingBundle` takeover of it, or a retry that would change the runtime, first takes the snapshot and records it with the pending deployment. A recovery point without the gateway store (a snapshot taken before the CLI captured it, or none after an older CLI's takeover) blocks any rollout that would start a gateway image the store has not run, as the runtime receipt and the gateway container record it: the apply is refused before anything changes, and the pending receipt stays as it was, because a snapshot taken now could already hold a migrated store. Do not remove the receipt. While no runtime rollout is pending, a reviewed bundle that still resolves the gateway image the deployment last ran can take it over with `supersedesPendingBundle`; the rollout after it takes a snapshot that holds the gateway store. Once a runtime rollout is pending, the CLI has no way forward for this state. A rollout on a gateway image the store has already run completes, and warns when its snapshot leaves the store out, or when it has no recovery point at all. A managed deployment never rolls back on its own and restores no volume; it keeps its pending state for recovery.
Managed bundle commands are unavailable on Windows, including `deploy verify-bundle` and backend-local `deploy provision`: their custody checks require POSIX executable modes. Run the complete managed deployment on a Linux host. Ordinary workspace commands and standalone `config build`, `verify`, `stage`, `deploy` and `verify-native` remain available on Windows.
This synthetic specification targets an existing organization and project. Replace its public identifiers and set the named environment values. `revision` accepts a full commit SHA directly or an environment reference; credentials remain references and are resolved privately at the destination. `tlsMode: "external"` means an existing edge handles public TLS; `letsencrypt` additionally requires `tlsEmail`.
For Linux or macOS ARM64 GitHub Actions jobs, use Tale's `.github/actions/setup-cli` composite action. Pin the action itself to a full Tale commit and pass that full commit as its `revision` input. It builds with Bun 1.4.2, verifies the final executable, returns `executable` and adds the binary to `PATH`. macOS builds support general configuration preparation; managed Linux stack preparation still requires a matching Linux executable. Set `linux-baseline: 'true'` on a Linux x64 runner when the destination CPU lacks AVX2 (Intel before Haswell, for example): the default executable aborts there with `Illegal instruction`; the baseline one runs. Other runners refuse the option.
`origin` and individual native `redirectUris` can also use environment references, so a deployment registry can own public addresses. Preparation resolves them to literal validated HTTPS URLs in the bundle.
The example sets `runtime.containerPrefix` to make its environment recognizable in container listings. This optional setting is explained below.
```json
{
"schemaVersion": 1,
"name": "example-native",
"stateDirectory": "/opt/tale-example",
"composeProject": "tale-example",
"runtime": {
"revision": { "env": "TALE_RUNTIME_REF" },
"platform": "linux/amd64",
"containerPrefix": "north-desk-prod"
},
"origin": { "env": "TALE_PUBLIC_ORIGIN" },
"tlsMode": "external",
"identity": {
"email": { "env": "EXAMPLE_OPERATOR_EMAIL" },
"password": { "env": "EXAMPLE_OPERATOR_PASSWORD" },
"slug": "example-team",
"name": "Example team",
"ssoEnabled": false,
"nativeClients": [
{
"key": "example-portal",
"name": "Example portal",
"clientId": { "env": "EXAMPLE_NATIVE_CLIENT_ID" },
"redirectUris": [{ "env": "EXAMPLE_PORTAL_CALLBACK" }]
}
]
},
"configs": [
{
"repository": "https://github.com/example-team/client-app",
"revision": { "env": "EXAMPLE_CONFIG_REF" },
"client": "example-team",
"descriptor": "tale/client.json",
"automation": "document-review",
"projectId": "existing-project-id",
"skillOwner": "native-operator-id"
}
]
}
```
#### Choose the error reporting environment
Managed runtime events default `SENTRY_ENVIRONMENT` to the retained deployment `name`. To use a canonical label such as `example-pr`, declare `"environment": { "SENTRY_ENVIRONMENT": { "env": "TALE_REPORTING_ENVIRONMENT" } }` and set that variable at the destination. The label starts with a lowercase letter or digit and contains 1–64 lowercase letters, digits or hyphens. Browser, backend and sandbox use this label without changing `name`, `composeProject`, `stateDirectory` or stored credentials.
#### Choose container names
Set `runtime.containerPrefix` when you want names such as `north-desk-prod-db` and `north-desk-prod-backend-api` in container listings. The prefix starts with a lowercase letter and uses lowercase letters, digits and single hyphens, up to 40 characters. Spaces, underscores, repeated hyphens and a trailing hyphen are refused.
| Setting | What it identifies |
| --- | --- |
| `runtime.containerPrefix` | Visible container names: `-` for every managed service. |
| `name` and `composeProject` | The retained deployment and its Compose project, including volume ownership. |
| `stateDirectory` | The existing deployment state, credentials and recovery records. |
Keep the last two rows unchanged when renaming an existing deployment's containers. The prefix leaves service DNS names unchanged, so internal service addresses continue to use their existing names. Omit it to use the source's container naming policy.
Adding, changing or removing a prefix recreates containers and can briefly interrupt service. Prepare a new bundle, review its dry run, then apply it through the normal snapshot and recovery flow. If application is interrupted, retry that exact bundle; a pending rollout refuses a different bundle. Once the rollout is ready, removing the prefix through another prepared bundle restores the source naming policy.
Run one complete managed runtime per Docker daemon. A name prefix does not allocate separate ports, sandbox networks or host workspaces.
#### Serve additional origins {#managed-additional-origins}
Declare `additionalOrigins` when the same instance also answers on other HTTPS origins, such as a partner domain or the previous hostname during a move. Each entry is a bare HTTPS origin on the default port, or an environment reference that preparation resolves to one. List 1 to 16 distinct origins; none may repeat `origin`.
```json
{
"origin": "https://desk.example.org",
"additionalOrigins": [
"https://desk.partner.example",
{ "env": "TALE_EXTRA_ORIGIN" }
]
}
```
The CLI writes the list to the runtime's `ADDITIONAL_SITE_URLS` and manages that variable, so an `environment` entry cannot set it. Every origin is a full entry point with its own sessions, file links, sign-in doors and connector callbacks. With `tlsMode: "letsencrypt"`, the proxy obtains a certificate for each origin, and local hostnames or IP addresses are refused. With `tlsMode: "external"`, your TLS proxy must forward each origin's original `Host` and send `X-Forwarded-Proto: https` from an address the Tale proxy trusts. When that address range is narrower than the private ranges, set `TRUSTED_PROXIES` through an `environment` reference.
The native identity stays on `origin`: account and organization bindings, client journals, the OIDC issuer, passkeys and email links use it alone. An entry may equal `identity.migrateOriginFrom` to keep the previous hostname answering while a migration completes.
Preparation refuses a runtime revision whose proxy cannot trust an external TLS terminator and reports `Runtime does not serve additional origins`. Adding, changing or removing the list recreates the services that read it. After you remove the declaration, applying the next bundle removes the variable. Register each origin's callback URLs with your identity and connector providers, and plan DNS and certificates with [TLS and domains](https://docs.tale.dev/self-hosted/configuration/tls-and-domains#several-domains-at-once).
#### Name who may create organizations {#managed-organization-creators}
A managed deployment refuses organization creation at its proxy for everyone: the organization picker shows no **Create organization** entry, and `POST /api/auth/organization/create` answers 403. To let named people open further workspaces, declare `organizations.creators` — 1 to 64 distinct sign-in addresses, literal or as environment references that preparation resolves.
```json
{
"organizations": {
"creators": ["ops@example.org", { "env": "TALE_WORKSPACE_LEAD" }]
}
}
```
The CLI writes the list to the runtime's `TALE_ORGANIZATION_CREATORS` and manages that variable, so an `environment` entry cannot set it. With the declaration in place the proxy no longer refuses organization creation; the backend judges every caller against the list instead, answers anyone else with `403 ORGANIZATION_CREATION_FORBIDDEN`, and the app shows **Create organization** only to the people named. Addresses are matched case-insensitively, so two spellings of one address are refused as a duplicate.
The managed organization itself is unaffected: the deployment creates it during bootstrap, and a deployment's first organization is always allowed. Remove the declaration and apply the next bundle to restore the proxy refusal and remove the variable. The same variable works on a deployment you run yourself; see the [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference).
#### Prepare, verify, and apply the bundle
Set `TALE_DEPLOY_SPEC` to that JSON file, `TALE_DEPLOY_BUNDLE` to a new absolute output directory, and `TALE_CLI_COMMIT` to the compiled CLI's full commit. `DEPLOYMENT_COMMIT` is optional orchestration provenance; omit its flags when unused. Prepare and verify, transfer the whole directory to the destination, then preview and apply there with the same pinned CLI.
```bash
tale --json deploy prepare \
--spec "$TALE_DEPLOY_SPEC" \
--deployment-ref "$DEPLOYMENT_COMMIT" \
--output "$TALE_DEPLOY_BUNDLE"
tale --json deploy verify-bundle \
--bundle "$TALE_DEPLOY_BUNDLE" \
--cli-ref "$TALE_CLI_COMMIT" \
--deployment-ref "$DEPLOYMENT_COMMIT"
tale --json deploy --bundle "$TALE_DEPLOY_BUNDLE" \
--cli-ref "$TALE_CLI_COMMIT" \
--deployment-ref "$DEPLOYMENT_COMMIT" --dry-run
tale --json --yes deploy --bundle "$TALE_DEPLOY_BUNDLE" \
--cli-ref "$TALE_CLI_COMMIT" \
--deployment-ref "$DEPLOYMENT_COMMIT"
```
`deploy prepare` accepts optional `--sources-file ` mapping `repository@fullSHA` to an existing exact checkout. Otherwise it fetches canonical GitHub repositories. Inject the read-only private-client SSH key contents through `TALE_SOURCE_SSH_KEY` only during preparation; the CLI verifies GitHub's SSH host keys over HTTPS and keeps the key out of the bundle and runtime. Registry access must already be available to Docker.
Preparation checks each configuration first, with this CLI's own schemas, and only then pulls and verifies the runtime images. It names each phase and image as it goes. A pack that declares fields this CLI does not know is refused within seconds with `native manifest normalization changes release semantics at `; prepare it with a CLI at least as new as the Tale the pack targets. A failure the CLI raises deliberately shows its own cause. Any other error keeps a fixed summary, so no credential or registry output reaches the log.
`deploy verify-bundle` checks the complete file inventory and hashes without a destination. `deploy --bundle --dry-run` checks configuration artifacts and destination preconditions without applying changes. Managed bundle deployment does not accept workspace-only overrides such as `--services`, `--host` or `--override-all`. It is a state-preserving stack rollout with health and provenance checks; the workspace blue-green behavior described above is a separate path.
#### Provision the native identity
`deploy provision [--bundle ]` is the backend-local phase normally invoked by bundle deployment. It reads at most 64 KiB of private JSON from stdin, proves the local account and selected organization, and always signs out before reporting success. Its fields include `origin`, `email`, `password`, `slug`, `name`, `ssoEnabled`, optional Entra credentials, and `nativeClients`. Existing-account behavior remains the default. An explicit `identity.bootstrap: "fresh"` permits creation of the initial local account and organization. A bundle binds this choice and the staged configurations before native changes. `deploy provision` refuses workspace flags and `--dry-run`; use read-only bundle/config verification for review. Its optional `--cli-ref` and `--deployment-ref` expectations require `--bundle` and are checked before login.
For an administratively verified fresh operator, explicitly declare `identity.emailVerification: "operator-attested"`. This is an operator assertion of the authenticated account’s email ownership, not proof of mailbox delivery. The backend uses a short-lived native verification token bound to that exact account and email, retaining native hooks without sending email, changing the address or creating another session. It is permitted only with `bootstrap: "fresh"`. Omit it to retain normal native email verification. A previously ready account whose verification changes holds for review.
For a fresh target, replace a config's `projectId` with `project: { "key": "NORTH", "name": "Configuration" }`. Native project keys have 2–6 uppercase letters and names at most 80 characters. `skillOwner: "operator"` transfers a verified source capsule and compiles it for the authenticated native user inside the backend; the host independently checks the resulting artifact. Existing explicit IDs and owner-bound releases retain their exact behavior.
Each native client chooses an existing `clientId` or explicit `managed: true`. Managed creation persists its private intent before the native request, then returns only a private handoff path and SHA for client credentials. Replays preserve IDs, security policy and secrets; uncertain request acceptance without a matching native object holds. Existing clients converge only their display name and HTTPS callback URLs. On maintained 0.5 backends, necessary create/update operations use fixed backend-local auth adapters and close their connections. This enables no public registration/update route, arbitrary module path or secret rotation.
#### Change a managed deployment’s hostname {#managed-origin-migration}
Use the retained managed deployment and its private state directory. This changes the origin bindings of the existing account, organization and clients; it does not move a database or create replacement identities.
1. Set the deployment specification’s `origin` to the new HTTPS origin and `identity.migrateOriginFrom` to the exact previous HTTPS origin, for example `https://old.example.org`. The two origins must differ. Keep `identity.bootstrap: "fresh"`, the same account and organization, and the same managed client keys.
2. Check the retained state before preparing the bundle. Bootstrap must be complete. Every declared email attestation and managed client needs its matching completed journal. Missing, pending or unrelated identity/client journals block migration.
3. Prepare, verify, preview and apply the bundle through the workflow above. The CLI authenticates the retained account and verifies client credentials before updating origin bindings. Completed journals at either declared origin are accepted on retry, preserving IDs and secrets.
4. After the deployment receipt is ready, export consumer configuration for the new issuer. Remove `migrateOriginFrom` from future deployment specifications.
When native configuration is declared, its retained receipt is required too. Migration preserves the organization ID and slug and checks every resource through the normal plan and readback flow. An interrupted configuration write at the new origin resumes only its exact pending plan. A pending configuration receipt at the old origin blocks migration.
To reverse a completed migration, explicitly swap the two origins and repeat the verified deployment workflow. Plan DNS, certificates, callback registrations and access checks with [TLS and domains](https://docs.tale.dev/self-hosted/configuration/tls-and-domains); changing the bundle origin does not perform those external changes.
#### Rename the deploy operator’s address {#managed-operator-address-migration}
A managed deployment signs in as its `identity` operator on every run. To give that account a machine address, so that people sign in with accounts of their own, keep the account and change only its sign-in address. Its user ID, and everything bound to it — managed clients, operator-owned skills, API keys and retained journals — stays.
1. Set `identity.email` to the new address and `identity.migrateEmailFrom` to the exact previous address. The two must differ. Keep `identity.bootstrap: "fresh"` and the account’s password. Bootstrap must be complete, and a declared email attestation needs its completed journal.
2. Prepare, verify, preview and apply the bundle. The CLI reads the retained account’s current address inside the backend, signs in with it and proves the retained user ID. It journals the change, renames the account through the native adapter, ends every session the account holds and signs in again with the new address. The rename is guarded by the previous address, so a concurrent change is refused rather than overwritten. A declared email attestation then verifies the new address.
3. A retry after an interruption finds the address already moved and completes the journals without a second rename. Keeping `migrateEmailFrom` declared afterwards is harmless; remove it once the deployment receipt is ready.
Another account holding the new address, a retained account holding neither address, or a single sign-on link on the operator account stops the deployment before any change. Remove such a link first: it belongs to the person who signed in with it, not to a machine account.
#### Declare a break-glass administrator {#managed-break-glass}
`identity.breakGlass` keeps one administrator for when the deploy operator is unavailable, for example `{ "email": "break-glass@example.org", "passwordHash": { "env": "TALE_BREAK_GLASS_PASSWORD_HASH" } }`. The address is a literal or a required environment reference and must differ from the operator’s current and previous address. The password never reaches the deployment: create its hash where the password is kept, with `tale auth hash-password`, and supply only the hash.
Every deployment makes the backend match the declaration. An absent account is created with a verified address and exactly the declared credential, and a retained journal binds the address to its account ID as soon as the account exists. Later deployments set the bound account back to the declared credential and end every session it held whenever the credential changes, also when they finish an interrupted run. An account at the address that this deployment did not create, or a different account holding it later, stops the deployment. The account becomes an `admin` of the managed organization through the native member endpoints; an `owner` is never changed. The deployment never signs in as this account. Rotate its password by changing the declared hash, not in the application. The deployment records no password-change time, so an organization password rotation policy may ask this account for a new password, which the next deployment sets back.
#### Enforced two-factor sign-in
A managed deployment signs in as its operator with the password alone. When the organization enforces `two_factor_policy`, give the operator a passkey and never an authenticator app: a password sign-in of an account with an authenticator app is answered with a challenge, and an account with neither factor is held at enrolment once its grace period ends. The CLI stops at either answer and names the one it received. People sign in with their own accounts and may use either factor.
#### Export native-client credentials
To hand a managed client’s credentials to a separate application, set `NATIVE_CLIENT_KEY` to its declared key and `PRIVATE_EXPORT_DIRECTORY` to a new private output directory. Its parent must already belong to your account, have mode `0700` and have trusted ancestors. Export from the same ready deployment without interpreting backend paths or container names:
```bash
tale --json deploy export-client --bundle "$TALE_DEPLOY_BUNDLE" \
--client "$NATIVE_CLIENT_KEY" --output "$PRIVATE_EXPORT_DIRECTORY" \
--env-prefix TALE_OIDC --cli-ref "$TALE_CLI_COMMIT" \
--deployment-ref "$DEPLOYMENT_COMMIT"
```
The output directory has mode `0700`. Its regular `0600` files are `client.json`, `receipt.json` and, when `--env-prefix` is selected, `consumer-env.json`. The latter is a literal four-string map: `TALE_OIDC_ISSUER`, `TALE_OIDC_CLIENT_ID`, `TALE_OIDC_CLIENT_SECRET` and `TALE_OIDC_ORG_SLUG`. The issuer is the Tale origin followed by `/api/auth`. Transfer these bytes through your private credential channel and let the application read JSON; do not source the file as shell or publish it as a CI artifact. Stdout contains only safe metadata, paths, sizes and hashes. An identical export is reused only after current ready-state and complete artifact checks; partial, stale or foreign output holds without overwrite.
### Configure the platform
Use `tale config` to manage existing platform settings through the native APIs. Save this declaration as `configuration.json` to set the accent color and a 45-minute idle timeout:
```json
{
"schemaVersion": 1,
"resources": [
{
"kind": "branding",
"config": {
"accentColor": "#336699"
}
},
{
"kind": "governance",
"key": "session_idle_timeout",
"config": {
"enabled": true,
"idleTimeoutMinutes": 45
}
}
]
}
```
Set `TALE_URL` to the instance’s HTTPS origin and `TALE_ORG_ID` to the native organization ID. Provide an authorized session cookie through `TALE_CONFIG_COOKIE`; keep it out of arguments and committed files. Validate locally, save and review the plan, then apply it and compare native state:
```bash
tale --json config validate --file configuration.json
tale --json config plan --file configuration.json \
--url "$TALE_URL" --org "$TALE_ORG_ID" --output configuration-plan.json
tale --json --yes config apply --file configuration.json \
--url "$TALE_URL" --org "$TALE_ORG_ID" \
--plan configuration-plan.json --receipt configuration-receipt.json
tale --json config read --file configuration.json \
--url "$TALE_URL" --org "$TALE_ORG_ID"
```
The output and receipt directories must already exist. A loopback HTTP connection also needs `--origin` with the instance’s public HTTPS origin. `read` reports `matches` for each declared resource. The plan identifies organization versus instance scope, current and desired hashes, and native side effects. Applying requires the exact declaration and target; a conflicting native edit refuses the write instead of overwriting it. Undeclared resources remain unchanged. There is no delete or arbitrary file-writing command.
These resource kinds use the platform’s shared schemas and native permissions:
| Kind | Configuration | Scope |
| --------------------- | ---------------------------------------------------------- | ------------ |
| `branding` | Native branding fields | Organization |
| `governance` | A file-backed policy `key` and its native `config` | Organization |
| `provider` | A custom provider definition and optional `expectedModels` | Organization |
| `provider-credential` | Named environment credential metadata | Organization |
| `knowledge-embedding` | Provider, model, dimensions, endpoint and server limits | Organization |
| `deployment` | Instance deployment settings, including sandbox runtime | Instance |
Retention and DSAR policies require their dedicated native workflows. Pause uploads, synchronization and crawls before changing the embedding model. The CLI checks organization-wide document and website counts; it does not lock ingestion or migrate existing vectors. For an organization with documents or registered websites, a model change requires a separate native indexing migration. A change limited to `minSimilarity`, `maxConcurrentRequests`, `minTokensPerSecond`, `maxTokensPerMinute` or `maxRequestsPerMinute` keeps existing vectors valid, so it skips this check. Instance settings also require the native deployment editor allowlist. Standalone application reports `restartRequired` for boot settings; saving those settings alone does not activate them. Review the plan’s effects before applying.
Managed deployments use the same engine through `configuration`. Merge this example into the deployment declaration when an external operator already serves the provider. Replace the synthetic endpoint and catalog with verified values, and inject `EXTERNAL_PROVIDER_SECRET` from your secret manager:
```json
{
"environment": {
"TALE_PROVIDER_KEY_EXTERNAL": {
"env": "EXTERNAL_PROVIDER_SECRET"
}
},
"configuration": {
"schemaVersion": 1,
"resources": [
{
"kind": "provider",
"config": {
"name": "external-chat",
"displayName": "External chat",
"apiFormat": "openai",
"baseUrl": "https://models.example.invalid/v1",
"catalog": {
"source": "models-endpoint"
},
"embedding": "unknown",
"auth": [
{
"method": "env"
}
]
},
"expectedModels": [
{
"id": "Example-chat",
"provider": "external-chat",
"tags": ["chat"],
"supportsTools": true,
"supportsVision": false,
"contextWindow": 131072
}
]
},
{
"kind": "provider-credential",
"config": {
"providerSlug": "external-chat",
"authMethod": "env",
"name": "Managed external provider",
"envName": "TALE_PROVIDER_KEY_EXTERNAL",
"modelAllowlist": ["Example-chat"]
}
}
]
}
}
```
`envName` uses the native `TALE_PROVIDER_KEY_` prefix and 40-character limit. Each alias needs a required `environment` reference. Private endpoints additionally require an explicit `TALE_ALLOW_PRIVATE_PROVIDER_HOSTS` reference whose value is `1`; native host restrictions still apply. `expectedModels` checks Tale’s freshly resolved catalog during readback. It does not prove inference capacity, latency or business output.
Use `governance` with `key: "vision_model"` and native `providerSlug`/`modelId` fields for vision selection. Embedding uses `knowledge-embedding` with `providerSlug`, `model`, `dimensions` and `baseUrl`, plus the optional settings the platform keeps next to the model in [`embedding.json`](https://docs.tale.dev/self-hosted/configuration/data-residency#the-organizations-embedding-model): `minSimilarity`, the assistant's cosine floor for this model, and the server limits `maxConcurrentRequests`, `minTokensPerSecond`, `maxTokensPerMinute` and `maxRequestsPerMinute` ([Pace requests to a self-hosted embedding server](https://docs.tale.dev/self-hosted/configuration/data-residency#embedding-server-capacity)). Each follows the platform's own rule: a value sets it, an omitted key leaves whatever the file holds (a hand-set value survives a release that does not mention it), and `null` clears it — `"minSimilarity": null`, for example, is the only way to remove a floor through the CLI. To replace a default credential, also declare the existing environment credential with `isDefault: false`; the CLI applies that explicit change first. Credential values never enter the declaration or receipt.
Native provisioning runs after identity and before configuration releases. The `native.configuration` receipt binds the declaration and bundle hashes, organization, resource hashes and native revisions. Writes retain a pending receipt before the first change; if a later resource fails, earlier changes may remain. Read the native state and retained receipt, then retry the same reviewed plan. Native compare-and-set protects each resource against concurrent admin changes; there is no cross-resource transaction. Keep deployment state, snapshots and receipts for recovery.
Managed deployments also activate a declared `deployment` resource before reporting ready. The CLI records the pending activation, drains the verified sandbox spawner for up to five minutes, and restarts that container once sessions have finished. If sessions remain, the operation stays pending. The `configurationActivation` receipt records the mounted configuration and observed container boot; ready requires fresh health checks. Retrying an interrupted operation verifies an already accepted restart, and an unchanged ready replay does not restart the service again.
#### Replace a pending configuration plan
If an interrupted plan can still complete, retry that same plan. Use explicit replacement when its declared settings can no longer work, for example because an embedding endpoint is no longer available. Keep the existing receipt: it records writes that may already have reached the platform.
1. Read the pending receipt and compare the declared resources with current native state. Prepare `replacement-configuration.json` with corrected settings, the same target and exactly the same resource identities. You cannot use replacement to add or drop resources.
2. Calculate the hash of the retained receipt’s `plan`, not the whole receipt or the replacement plan. The following command requires Bun and hashes canonical JSON: object keys sorted recursively, array order preserved, and no whitespace.
```bash
PENDING_PLAN_SHA=$(bun -e '
const receipt = await Bun.file(process.argv[1]).json();
if (receipt.phase !== "pending") throw new Error("Receipt is not pending");
function canonical(value) {
if (Array.isArray(value)) return "[" + value.map(canonical).join(",") + "]";
if (value !== null && typeof value === "object") {
return "{" + Object.keys(value).sort().map(key =>
JSON.stringify(key) + ":" + canonical(value[key])
).join(",") + "}";
}
return JSON.stringify(value);
}
console.log(new Bun.CryptoHasher("sha256")
.update(canonical(receipt.plan)).digest("hex"));
' configuration-receipt.json)
```
3. Create a fresh plan from the replacement declaration and review it before applying:
```bash
tale --json config plan --file replacement-configuration.json \
--url "$TALE_URL" --org "$TALE_ORG_ID" --output replacement-plan.json
```
After reviewing the plan, apply it with the original receipt path and the retained plan’s hash:
```bash
tale --json --yes config apply --file replacement-configuration.json \
--url "$TALE_URL" --org "$TALE_ORG_ID" \
--plan replacement-plan.json --receipt configuration-receipt.json \
--supersedes-pending-plan "$PENDING_PLAN_SHA"
tale --json config read --file replacement-configuration.json \
--url "$TALE_URL" --org "$TALE_ORG_ID"
```
Each resource must still match the pending plan’s original state, intended write, or verified result. An unrelated native edit blocks replacement before any write. Resolve that difference with the administrator who made it; do not remove the receipt to bypass the check.
The receipt retains the previous plan and its verified resources under `superseded`, including across interruption and replay. Check that the receipt reaches `phase: "ready"` and that `config read` reports matching resources. Omit the one-shot selector from later operations.
For a managed deployment, set `supersedesPendingConfigurationPlan` to the same retained plan hash in the deployment specification, alongside the corrected `configuration`, then prepare and review a new bundle. If that bundle also replaces a pending rollout, select its hash separately with `supersedesPendingBundle`. That bundle selector alone does not authorize replacing a native configuration plan. Remove both recovery selectors from later specifications after the operation is ready.
The public `native.configuration` proof records each resource’s intended hash as `configurationSha256` and its native readback hash as `observedConfigurationSha256`. These can differ when a setting preserves an existing value, such as an omitted embedding similarity floor or server limit. The private receipt retains the exact observed state for recovery.
### Operate
`tale status` — show the current deployment status. No arguments.
`tale logs ` — stream a service's logs (`service` is one of the running services; on a dev-only stack with no deployment, it falls back to the dev container).
- `-f, --follow` — follow log output as it is written.
- `-n, --tail ` — show only the last N lines.
- `--since ` — show logs since a relative time (e.g. `1h`, `30m`).
- `-c, --color ` — target a specific deployment colour (`blue` or `green`).
- `--raw` — stream raw, unfiltered log output (no classification).
`tale backup` — snapshot the supported, existing project volumes. No arguments. External databases and buckets need separate backups; see [Backup coverage](https://docs.tale.dev/self-hosted/operate/backups-and-restore).
`tale restore [snapshot-id]` — restore a snapshot; omit the id to list available snapshots.
- `--stop` — stop running project containers before restoring.
- `-y, --yes` — skip the confirmation prompt.
`tale rollback` — roll back to the previous patch version (patch-level only). Prompts for confirmation before it touches anything.
- `-y, --yes` — skip the confirmation prompt (required when running non-interactively).
### Maintain
`tale update`—move a workspace instance to a new version: update the CLI binary, then sync project files; run `tale deploy` afterward. Workspace commands align to that version. Managed bundles and configuration releases retain their separately pinned CLI revision.
- `-v, --version ` — update to this exact version (e.g. `0.9.0`) instead of the latest; allows downgrades.
- `-f, --force` — force re-sync and overwrite locally modified project files.
- `--dry-run` — show what would change without modifying anything.
`tale migrate` — re-provision the built-in defaults for every organization against the running deployment — the same idempotent step every deploy runs, on demand. Schema migrations are not a command: the backend applies them at boot, so a deployed container is always at its own schema.
- `--dry-run` — show what would run without executing it.
`tale cleanup` — remove inactive (non-current colour) containers. No arguments.
`tale reset` — remove all blue-green containers.
- `-f, --force` — skip the confirmation prompt.
- `-a, --all` — also remove the stateful infrastructure containers.
- `--dry-run` — preview the reset without making changes.
`tale uninstall` — remove the `tale` CLI binary from this system. It prompts before deleting anything and _offers_ to also disconnect this machine's [sandbox device](https://docs.tale.dev/self-hosted/install/cli-install#sandbox-device), remove `~/.tale-daemon` left by the retired `tale daemon`, and tear down a project's Docker resources and files. Without `--purge`, a project and its containers are left intact — run `tale reset --all` inside one to remove those. If the sandbox device cannot be stopped, nothing is uninstalled.
- `-f, --force` — skip the confirmation prompt (removes the binary only; the optional cleanups still need `--purge`).
- `--purge` — also disconnect the sandbox device and delete its workspaces, remove `~/.tale-daemon` and, for a project found from the current directory, tear down its Docker resources and delete its files. Irreversible.
- `--dry-run` — show what would be removed without removing anything.
`tale config show`—print the resolved local project directory and CLI version. Outside a project, it reports that no project was found and exits successfully.
### Configuration releases
These commands use the selected CLI revision without instance alignment or Docker operations. [Release client configurations](https://docs.tale.dev/self-hosted/configuration/config-releases) covers descriptors, source commits, credentials and recovery. The default release identity is the full source SHA: manifest schema 4/compiler 3, with `releaseRef === sourceCommit`. Native integer automation versions remain separate.
`build`, `verify` and `stage` require `--repo `, `--descriptor ` and `--automation `. The descriptor is repository-relative. A verification manifest may be absolute or repository-relative.
| Command | Required options | Optional options |
| -------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `tale config build` | `--source-commit ` | `--skill-owner ` (required for owned skills), `--output `, compatibility `--config-version ` |
| `tale config verify` | `--manifest ` | `--rebuild` for exact offline reconstruction |
| `tale config stage` | `--config-ref `, `--output ` | `--skill-owner `, `--client `, `--deployment-ref ` |
Source staging requires checkout `HEAD` at that full commit and output outside the checkout. It rebuilds committed content without a generated catalogue commit. Explicit `stage --config-version` instead selects the compatibility catalogue path and requires `--catalogue-commit`, `--catalogue-repository`, `--client` and `--ops-commit`. Do not mix `--config-ref` and `--config-version`.
Native commands require `--stage `, `--url `, `--org ` and `--project `. HTTPS is required except for loopback HTTP; use `--origin ` for the canonical browser origin behind a proxy. Both read `TALE_CONFIG_COOKIE` only from the environment.
| Command | Required options | Optional options |
| --------------------------- | ------------------------- | ------------------------------------------------------ |
| `tale config deploy` | `--receipt ` | Global `--yes` for an authorized unattended deployment |
| `tale config verify-native` | No extra required options | `--native-version `, `--allow-retained` |
Both native commands accept exact expectations through `--config-ref`, `--source-repository`, `--artifact-sha256`, `--deployment-ref`, `--client` and `--automation`. Historical catalogue expectation flags remain available for compatibility. `verify-native` is read-only; `--allow-retained` verifies an explicitly selected retained version without claiming it is deployed. Without `--native-version`, verification selects the latest saved version. The native API exposes the task contract only for the deployed version, so retained verification cannot attest that field.
Configuration commands have no `--dry-run`: use `stage`, `verify --rebuild` and `verify-native`. Success JSON is `{ok:true,command:"config ",data}`. Build and verify data include `automationName`, `releaseRef`, `sourceCommit`, `artifactSha256`, `artifactPath` and `verified`; compatibility output uses `configVersion` instead of `releaseRef`. SHA stage and native receipts use schema 2. A deploy result includes `automationVersion` and `unchanged`; the explicit `verified` field belongs to verification output.
### Sandbox device
These commands run your Tale organization's sandboxes on the machine where you run them. [Run sandboxes on your own devices](https://docs.tale.dev/platform/admin/sandbox-devices) covers adding a device under **Settings > Sandboxes**, which hands out the `connect` command. The device keeps its configuration and workspaces in `~/.tale/sandbox`; `TALE_SANDBOX_HOME` moves them.
`tale sandbox connect --token ` — connect this machine to the organization that issued the token: it checks Docker (and offers to install it), registers the machine, and starts the device's containers at the server's release. `site` is your Tale site, such as `https://your-org.tale.dev`. The token (`tsdj_…`) works once, within an hour. Linux and macOS only.
- `--name ` — the name Tale shows for the device (default: the machine's hostname). Characters other than `A`–`Z`, `a`–`z`, digits, `.`, `-` and `_` become dashes.
- `--max-sessions ` — how many sandboxes run here at once, from 1 to 256 (default: one per two CPUs and per 4 GiB of memory Docker can use, at most 16).
- `--no-auto-update` — do not follow the server's release automatically; run `tale sandbox update` after each Tale update instead.
- `--docker-socket ` — the Docker socket the device's containers use, for rootless Docker (default: `/var/run/docker.sock`).
`tale sandbox status` — show the device's connection, organization, release and containers, and the sandboxes running now. No arguments.
`tale sandbox update` — move the device to the server's release now, and take the server's current addresses for its sandboxes (needed after the backend or model gateway address changed). No arguments.
`tale sandbox logs` — show the device's log.
- `-f, --follow` — keep printing new lines.
- `--tail ` — how many lines to show first (default: `200`).
`tale sandbox disconnect` — remove the device from its organization, stop its sandboxes and delete their workspaces from this machine. It prompts first, and changes nothing while Docker is not running. If the server cannot be reached, the machine is cleaned up anyway, and an admin removes the device under **Settings > Sandboxes**.
- `--keep-data` — keep the workspaces on this machine.
- `-f, --force` — skip the confirmation prompt.
### Advanced
`tale auth hash-password` — print the Better Auth hash of a password, for a [break-glass administrator](https://docs.tale.dev/self-hosted/install/cli-install#managed-break-glass).
It reads the password from stdin, or from a hidden prompt with confirmation in an interactive terminal. It refuses a password that fails the platform’s default password policy and prints nothing but the hash.
`tale auth reset-owner` — reset the owner account credentials.
For a manual recovery, run it without flags in an interactive terminal to enter the new password through a masked prompt. This avoids putting the password in shell history or command arguments. The reset invalidates existing sessions.
- `-e, --email ` — set a new owner email address.
- `-p, --password ` — set a new owner password.
## Troubleshooting
- **`tale deploy` targets the wrong machine.** The CLI uses your shell's Docker context / `DOCKER_HOST`. Switch with `docker context use …` (or set `DOCKER_HOST`) so it points at the intended host, then re-run.
- **`tale deploy` uses the wrong host alias.** The host the proxy answers on comes from `HOST` in the project's `.env`, not a separate CLI store. Edit `.env` or pass `--host` to override it for one run.
- **Installer fails on macOS because the binary cannot execute.** When the freshly installed binary refuses to run (e.g. Gatekeeper kills it), the installer fails with recovery hints instead of reporting success — follow them, then re-run the installer.
- **`tale` not found after install on Linux.** The installer drops the binary in `/usr/local/bin`; verify the directory is on the user's `PATH` (`echo $PATH`).
For ongoing operations, use [Upgrades](https://docs.tale.dev/self-hosted/operate/upgrades), [Backups and restore](https://docs.tale.dev/self-hosted/operate/backups-and-restore), or [Container architecture](https://docs.tale.dev/self-hosted/operate/container-architecture).
# Create the first owner account
Source: https://docs.tale.dev/self-hosted/install/first-admin
On an empty instance, Tale’s setup flow creates the first account and organization. That account becomes the Owner. Complete this step while you still control access to the new instance, before sharing its address with others.
## Confirm the instance is ready
Open the address printed by `tale dev`, or the configured `SITE_URL` for a deployment, and check the certificate and hostname. For a CLI deployment, run `tale status`; for a deployment you maintain yourself, inspect its services and probes. An unhealthy backend needs [troubleshooting](https://docs.tale.dev/self-hosted/operate/observability/troubleshooting) before account setup.
A login page instead of setup usually means an account already exists. This is expected in a seeded development environment. Do not erase the database to recover access; sign in with the existing account or ask an administrator to add your account.
## Complete setup
Open the instance URL. Follow the setup flow to create your account and name the organization. Keep your sign-in credentials in a password manager.
After creating the account and organization, use the setup completion page’s provider link or open **Settings > AI providers**. Without a provider you can inspect the app, but a real model reply still needs valid credentials and an available model. Follow [AI providers](https://docs.tale.dev/platform/admin/providers) when you are ready to connect one.
## Confirm ownership
Open **Settings > Members** and verify that your account has the **Owner** role. The organization name and account should match the instance you intended to initialize.

Sign out and sign in again to verify the credentials independently of the setup session. Keep another tested administrative recovery path before changing authentication settings.
## Add teammates {#invite-teammates}
Add people through **Settings > Members** and choose their roles deliberately. For a new account, the form sets an initial password; an existing account keeps its credentials. This flow does not send an invitation email. Share initial credentials through a secure channel. Public self-service registration closes after the first account. Corporate SSO and provisioning have their own [setup and membership rules](https://docs.tale.dev/platform/admin/enterprise-sso).
The backend enforces that, not only the proxy in front of it: once any account exists, `/api/auth/sign-up/email` answers 403. This matters because the backend is also reachable from the agent sandbox network, which the proxy never sees, so code running in an agent session cannot create accounts either. **Settings > Members** creates accounts server-side and is unaffected. A throwaway test deployment that needs the open route sets `TALE_ALLOW_OPEN_SIGN_UP=true`; never set it on a real one.
Creating a further organization is open to every signed-in user unless you name who may do it: set `TALE_ORGANIZATION_CREATORS` to their sign-in addresses, and everyone else loses the **Create organization** entry in the organization picker and is refused at the API with `403 ORGANIZATION_CREATION_FORBIDDEN`. The first organization is always allowed, so setup is not affected. A managed deployment declares the same list as `organizations.creators` in its specification; see [Install the tale CLI](https://docs.tale.dev/self-hosted/install/cli-install#managed-organization-creators) and the [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference).
Use [Members and roles](https://docs.tale.dev/platform/admin/members-and-roles) to choose access. Then [send your first message](https://docs.tale.dev/get-started/quickstart) and verify a completed reply. Create a [project agent](https://docs.tale.dev/tutorials/editor/first-agent-end-to-end) when you are ready to delegate a task. A working dashboard confirms access to the application; it does not verify the model provider or every background service.
# Choose an installation method
Source: https://docs.tale.dev/self-hosted/install
Use the Tale CLI for the standard workspace installation. Choose a custom stack when your infrastructure tooling must own the service definitions. Both paths need the same application services and an operator responsible for configuration and maintenance.
## Install with the CLI
The [quickstart](https://docs.tale.dev/self-hosted/install/quickstart) takes you through the prerequisites, project creation, startup, and first sign-in. `tale init` prepares a project directory; `tale dev` starts a development instance and `tale deploy` deploys that workspace.
The CLI manages container operations, but you still own the project's configuration, credentials, volumes, and updates. Keep the project directory and its deployment settings together. [Install the CLI](https://docs.tale.dev/self-hosted/install/cli-install) covers supported systems, remote Docker access, commands, and managed deployment options.
## Use your own service definitions
[Run your own stack](https://docs.tale.dev/self-hosted/install/own-compose) describes the services, volumes, networking, readiness checks, and boot order you must preserve. Use it when maintaining Compose yourself. [Deploy on Kubernetes](https://docs.tale.dev/self-hosted/install/kubernetes) translates that contract into Deployments, Services, and NetworkPolicies, and lists the checks a cluster must pass. Tale does not ship an official Helm chart.
For changes to Tale's source code, follow [Contributor setup](https://docs.tale.dev/develop/contributor-setup) instead of starting with a production deployment.
## Complete the first setup
After the instance is ready, [create the first administrator](https://docs.tale.dev/self-hosted/install/first-admin), connect a provider, and test a chat. Add team members through [Members and roles](https://docs.tale.dev/platform/admin/members-and-roles), using the account and sign-in options available in your organization.
Before using production data, configure TLS and backups, confirm the [environment settings](https://docs.tale.dev/self-hosted/configuration/environment-reference), and read the [operating architecture](https://docs.tale.dev/self-hosted/operate/container-architecture).
# Deploy on Kubernetes
Source: https://docs.tale.dev/self-hosted/install/kubernetes
Tale runs on Kubernetes when you translate the [service contract](https://docs.tale.dev/self-hosted/install/own-compose) into Deployments, Services, and volumes and switch the sandbox spawner to `SANDBOX_BACKEND=kubernetes`. There is no official Helm chart. This guide carries a complete manifest set for one namespace, verified end to end with Tale 0.5.31 on a single-node cluster, together with the checks that prove the result. You own the cluster, its storage, its public entry point, and the rollout procedure.
## Check the prerequisites
| Requirement | Why it matters |
| --- | --- |
| A CNI that enforces NetworkPolicy, such as Calico, Cilium, or kube-network-policies | The sandbox egress fence and the backend fence are NetworkPolicy objects. Every API server accepts them; only the CNI makes them block traffic. |
| A default StorageClass whose `ReadWriteOnce` volumes re-bind where a Pod is scheduled | The database, the object store, the proxy certificates, the gateway state, and every sandbox workspace live on PersistentVolumeClaims. A deleted sandbox workspace's volume follows the StorageClass's `reclaimPolicy`: with `Delete`, its files go with it; with `Retain`, they stay until you remove the volume. |
| `ReadWriteMany` storage, or a single node, for the organization configuration | The backend roles write `config-data`; the web tier and the spawner read it. `ReadWriteOnce` is enough on one node. Several nodes need `ReadWriteMany` or a node pin for those Pods. |
| Nodes that grant `NET_ADMIN` and provide ip6tables, or allow the IPv6 sysctls | The egress proxy installs its firewall at start and refuses to start without it. |
| Ports 80 and 443 reachable at the public address | Caddy obtains certificates itself in `selfsigned` and `letsencrypt` mode. Behind an Ingress that terminates TLS, set `TLS_MODE=external`. |
| Pull access to `ghcr.io/tale-project/tale/*` on every node, including the sandbox runtime image | Session Pods start from `SANDBOX_RUNTIME_IMAGE`. A node that cannot pull it fails the first session scheduled there. |
| A sysbox or kata RuntimeClass if agents need Docker inside their sandbox | Without one, keep `SANDBOX_DOCKER_IN_CONTAINER=false`. The `runc` tier would need privileged Pods. |
| `kubectl` and `envsubst` on the machine that applies the manifests | The manifests carry one `${VERSION}` variable that kubectl does not expand. |
Reserve memory for the application roles plus one agent session per concurrent task; `SANDBOX_AGENT_MEMORY` and the other session limits are in the [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference#sandbox-infrastructure).
## Lay out the namespace
Every service runs in one namespace, `tale`. Session Pods must reach `backend-api` and `sandbox-llm-gateway` directly, and the egress fence the spawner installs allows only the namespace it runs in, so the application roles belong there too. The Service names equal the Compose service names: the images resolve `db`, `knowledge-db`, `object-store`, `backend-api`, `platform`, `sandbox`, `sandbox-egress`, `sandbox-llm-gateway` with its `llm-gateway` alias, and `bgutil-provider` by name.
Every Pod below sets `enableServiceLinks: false`. Kubernetes otherwise injects Docker-style variables for each Service in the namespace, such as `SANDBOX_PORT=tcp://10.96.6.49:8003` and `DB_PORT=tcp://10.96.150.113:5432`. The spawner reads `SANDBOX_PORT` as its listen port and exits at start, and the platform image derives its database URL from `DB_PORT`.
| Compose service | Kubernetes objects | Notes |
| --- | --- | --- |
| `db` with the `knowledge-db` alias | StatefulSet `db`; Services `db` and `knowledge-db` selecting the same Pod | `TALE_DB_ROLE` stays unset: the image creates both databases and applies the knowledge migrations, and the backend migrates the application schema at boot. An in-memory `emptyDir` of 256 MiB serves `/dev/shm`. The image stops on `SIGINT` within 60 seconds of grace. |
| `object-store` | Deployment with strategy `Recreate`, PVC at `/data`, Service on 9000 | The backend creates the bucket at boot. |
| `platform` | Deployment; Service on 3000 | `config-data` read-only, `TALE_BACKEND_URL=http://backend-api:3005`. |
| `backend-api` | Deployment with two replicas; Service on 3005 | `config-data` read-write. Two replicas give a rollout without a gap. |
| `backend-worker` | Deployment | No Service and no HTTP probe. |
| `proxy` | Deployment with strategy `Recreate`; `hostPort` 80 and 443; PVC for `/data` | The certificate store survives restarts on the PVC. |
| `sandbox` | ServiceAccount, Role, RoleBinding, Deployment; Service on 8003 | `SANDBOX_BACKEND=kubernetes`; `config-data` read-only at `/app/platform-config`. No Docker socket. |
| `sandbox-egress` | Deployment; Service on 3128 | The shipped capability set, no sysctls. |
| `sandbox-llm-gateway` | Deployment with strategy `Recreate`, PVC at `/app/data`; Services `sandbox-llm-gateway` and `llm-gateway` on 8080 | The image runs as uid 1000; `fsGroup: 1000` lets it write its state. It reads `SANDBOX_LLM_GATEWAY_ADMIN_PASSWORD` from `tale-env`: until it has an admin account, it creates one only for a caller that presents that secret. |
| `bgutil-provider` | Deployment; Service on 4416 | Optional video-token provider. |
The probes translate the Compose health checks:
| Service | Startup probe | Readiness probe | Liveness probe |
| --- | --- | --- | --- |
| `backend-api` | `GET /ping` on 3005, allow five minutes for migrations | `GET /ready` on 3005 | `GET /ping` on 3005 |
| `platform` | `curl -sf http://localhost:3000/api/health && [ -f /tmp/platform-ready ]`, allow three minutes | the same command | none |
| `db` | `pg_isready -U tale -d tale && [ -f /tmp/.db_ready ]`, allow three minutes | the same command | `pg_isready -U tale -d tale` |
| `object-store` | none | `mc ready local` | none |
| `proxy` | none | `GET /health` on 2020 | none |
| `sandbox` | `GET /health` on 8003 | `GET /health` on 8003 | none |
| `sandbox-egress` | none | `curl -sS -o /dev/null --max-time 3 --noproxy '*' http://127.0.0.1:3128/` | none |
| `sandbox-llm-gateway` | none | `GET /health` on 8080 | none |
Kubernetes has no `depends_on`. A backend role that starts before Postgres answers exits once with `ECONNREFUSED`, and the restart policy heals it.
## Prepare the manifests
Save each YAML block in the following sections as the file named in its first line, in one directory. Edit the Secret: replace every `<...>` placeholder and set `HOST`, `SITE_URL`, `TLS_MODE`, and `OBJECT_STORE_PUBLIC_ENDPOINT` for your address. If that address carries a non-standard port, also set it as the proxy's `containerPort` and `hostPort` in `30-proxy.yaml`; Caddy listens on the port in `SITE_URL`. Then pin one release for every Tale image, `0.5.31` at the time of writing, and apply the files in order:
```bash
export VERSION=0.5.31
for f in 00-namespace.yaml 10-stores.yaml 20-application.yaml 30-proxy.yaml 40-sandbox.yaml; do
envsubst '${VERSION}' < "$f" | kubectl apply -f -
done
```
`envsubst` replaces only `${VERSION}`; every other value in the files is literal. The same loop applies an upgrade: change `VERSION`, run it again, and the Deployments roll to the new image.
## Create the shared environment
The first file holds the namespace, the deployment-wide values from the Compose `.env`, and the shared configuration claim. Generate each secret once and keep it; `ENCRYPTION_SECRET_HEX` in particular must stay stable for existing encrypted values. The [environment reference](https://docs.tale.dev/self-hosted/configuration/environment-reference) explains every variable.
```yaml
# 00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata: { name: tale }
---
apiVersion: v1
kind: Secret
metadata: { name: tale-env, namespace: tale }
type: Opaque
stringData:
HOST: tale.example.com
SITE_URL: https://tale.example.com
TLS_MODE: letsencrypt
POSTGRES_USER: tale
POSTGRES_DB: tale
DB_PASSWORD:
DATABASE_URL: postgresql://tale:@db:5432/tale_app
KNOWLEDGE_DB_NAME: tale_knowledge
INSTANCE_SECRET:
BETTER_AUTH_SECRET:
ENCRYPTION_SECRET_HEX:
TALE_AUDIT_PEPPER:
SANDBOX_TOKEN:
SANDBOX_LLM_GATEWAY_ADMIN_PASSWORD:
OBJECT_STORE_ACCESS_KEY: tale
OBJECT_STORE_SECRET_KEY:
OBJECT_STORE_BUCKET: tale-blobs
OBJECT_STORE_ENDPOINT: http://object-store:9000
OBJECT_STORE_REGION: us-east-1
OBJECT_STORE_PUBLIC_ENDPOINT: https://tale.example.com
---
# Organization configuration: backend roles write, platform and spawner read.
# ReadWriteOnce is enough on one node; several nodes need ReadWriteMany.
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: config-data, namespace: tale }
spec:
accessModes: [ReadWriteOnce]
resources: { requests: { storage: 2Gi } }
```
`DATABASE_URL` carries the same password as `DB_PASSWORD`; the knowledge connection defaults to `knowledge-db:5432/tale_knowledge` with that password. `SITE_URL` must match the address in the browser, including a non-standard port.
## Run the stores
The StatefulSet keeps the data volume across Pod replacements and gives the image the shutdown it expects. MinIO runs as a single Deployment on its own claim.
```yaml
# 10-stores.yaml
apiVersion: v1
kind: Service
metadata: { name: db, namespace: tale }
spec:
selector: { app: db }
ports: [{ name: pg, port: 5432, targetPort: 5432 }]
---
apiVersion: v1
kind: Service
metadata: { name: knowledge-db, namespace: tale }
spec:
selector: { app: db }
ports: [{ name: pg, port: 5432, targetPort: 5432 }]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: db, namespace: tale }
spec:
serviceName: db
replicas: 1
selector: { matchLabels: { app: db } }
template:
metadata: { labels: { app: db } }
spec:
enableServiceLinks: false
terminationGracePeriodSeconds: 60
containers:
- name: postgres
image: ghcr.io/tale-project/tale/tale-db:${VERSION}
envFrom: [{ secretRef: { name: tale-env } }]
ports: [{ name: pg, containerPort: 5432 }]
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
- { name: shm, mountPath: /dev/shm }
startupProbe:
exec: { command: [sh, -c, 'pg_isready -U tale -d tale && [ -f /tmp/.db_ready ]'] }
periodSeconds: 5
failureThreshold: 36
readinessProbe:
exec: { command: [sh, -c, 'pg_isready -U tale -d tale && [ -f /tmp/.db_ready ]'] }
periodSeconds: 5
livenessProbe:
exec: { command: [sh, -c, 'pg_isready -U tale -d tale'] }
periodSeconds: 15
resources:
requests: { cpu: 250m, memory: 512Mi }
volumes:
- name: shm
emptyDir: { medium: Memory, sizeLimit: 256Mi }
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: [ReadWriteOnce]
resources: { requests: { storage: 20Gi } }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: object-store-data, namespace: tale }
spec:
accessModes: [ReadWriteOnce]
resources: { requests: { storage: 20Gi } }
---
apiVersion: v1
kind: Service
metadata: { name: object-store, namespace: tale }
spec:
selector: { app: object-store }
ports: [{ name: s3, port: 9000, targetPort: 9000 }]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: object-store, namespace: tale }
spec:
replicas: 1
strategy: { type: Recreate }
selector: { matchLabels: { app: object-store } }
template:
metadata: { labels: { app: object-store } }
spec:
enableServiceLinks: false
terminationGracePeriodSeconds: 30
containers:
- name: minio
image: ghcr.io/tale-project/ops/minio:RELEASE.2025-04-22T22-12-26Z
args: ['server', '/data', '--address', ':9000', '--console-address', ':9001']
env:
- name: MINIO_ROOT_USER
valueFrom: { secretKeyRef: { name: tale-env, key: OBJECT_STORE_ACCESS_KEY } }
- name: MINIO_ROOT_PASSWORD
valueFrom: { secretKeyRef: { name: tale-env, key: OBJECT_STORE_SECRET_KEY } }
- { name: MINIO_BROWSER, value: 'off' }
ports: [{ name: s3, containerPort: 9000 }]
volumeMounts: [{ name: data, mountPath: /data }]
readinessProbe:
exec: { command: [sh, -c, 'mc ready local'] }
periodSeconds: 10
resources:
requests: { cpu: 100m, memory: 256Mi }
volumes:
- name: data
persistentVolumeClaim: { claimName: object-store-data }
```
For an external Postgres, set `DATABASE_URL` and `KNOWLEDGE_DATABASE_URL` as described in [Connect external stores](https://docs.tale.dev/self-hosted/install/own-compose#connect-external-stores) and drop the StatefulSet and its Services; for an external bucket, set the `OBJECT_STORE_*` values and drop the MinIO objects.
## Run the application roles
The three roles share the platform image: the API and the worker write `config-data`, the web tier reads it. The file also carries the optional video-token provider the worker uses; remove its two objects if you do not ingest videos.
The backend roles run without `NET_ADMIN` and with `TALE_SKIP_SSRF_FIREWALL=1`. With that capability the image installs its iptables egress fence, which accepts only the subnets the Pod is directly connected to and rejects the rest of the private address space. On a Pod network that rejects the cluster DNS and every Service address, so the role fails with `getaddrinfo EAI_AGAIN db` and restarts until you remove the capability. The NetworkPolicy at the end of the file provides the fence instead: the roles reach every peer in the namespace, the cluster DNS, and the public internet, and never the cloud metadata service, the nodes, or private networks. Extend its last rule when your model providers or connectors live on a private range.
```yaml
# 20-application.yaml
apiVersion: v1
kind: Service
metadata: { name: backend-api, namespace: tale }
spec:
selector: { app: backend-api }
ports: [{ name: http, port: 3005, targetPort: 3005 }]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: backend-api, namespace: tale }
spec:
replicas: 2
selector: { matchLabels: { app: backend-api } }
template:
metadata: { labels: { app: backend-api, tale.tier: backend } }
spec:
enableServiceLinks: false
terminationGracePeriodSeconds: 45
containers:
- name: backend-api
image: ghcr.io/tale-project/tale/tale-platform:${VERSION}
envFrom: [{ secretRef: { name: tale-env } }]
env:
- { name: TALE_ROLE, value: api }
- { name: PORT, value: '3005' }
- { name: TALE_CONFIG_DIR, value: /app/data }
- { name: SANDBOX_URL, value: http://sandbox:8003 }
- { name: SANDBOX_HTTP_API_BASE_URL, value: http://backend-api:3005 }
- { name: TALE_SKIP_SSRF_FIREWALL, value: '1' }
ports: [{ name: http, containerPort: 3005 }]
volumeMounts: [{ name: config-data, mountPath: /app/data }]
startupProbe:
httpGet: { path: /ping, port: 3005 }
periodSeconds: 5
failureThreshold: 60
readinessProbe:
httpGet: { path: /ready, port: 3005 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /ping, port: 3005 }
periodSeconds: 10
resources:
requests: { cpu: 500m, memory: 1Gi }
volumes:
- name: config-data
persistentVolumeClaim: { claimName: config-data }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: backend-worker, namespace: tale }
spec:
replicas: 1
selector: { matchLabels: { app: backend-worker } }
template:
metadata: { labels: { app: backend-worker, tale.tier: backend } }
spec:
enableServiceLinks: false
terminationGracePeriodSeconds: 45
containers:
- name: backend-worker
image: ghcr.io/tale-project/tale/tale-platform:${VERSION}
envFrom: [{ secretRef: { name: tale-env } }]
env:
- { name: TALE_ROLE, value: worker }
- { name: TALE_CONFIG_DIR, value: /app/data }
- { name: SANDBOX_URL, value: http://sandbox:8003 }
- { name: SANDBOX_HTTP_API_BASE_URL, value: http://backend-api:3005 }
- { name: TALE_SKIP_SSRF_FIREWALL, value: '1' }
volumeMounts: [{ name: config-data, mountPath: /app/data }]
resources:
requests: { cpu: 500m, memory: 1Gi }
volumes:
- name: config-data
persistentVolumeClaim: { claimName: config-data }
---
apiVersion: v1
kind: Service
metadata: { name: platform, namespace: tale }
spec:
selector: { app: platform }
ports: [{ name: http, port: 3000, targetPort: 3000 }]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: platform, namespace: tale }
spec:
replicas: 1
selector: { matchLabels: { app: platform } }
template:
metadata: { labels: { app: platform } }
spec:
enableServiceLinks: false
terminationGracePeriodSeconds: 45
containers:
- name: platform
image: ghcr.io/tale-project/tale/tale-platform:${VERSION}
envFrom: [{ secretRef: { name: tale-env } }]
env:
- { name: TALE_BACKEND_URL, value: http://backend-api:3005 }
- { name: TALE_CONFIG_DIR, value: /app/data }
ports: [{ name: http, containerPort: 3000 }]
volumeMounts: [{ name: config-data, mountPath: /app/data, readOnly: true }]
startupProbe:
exec: { command: [sh, -c, 'curl -sf http://localhost:3000/api/health && [ -f /tmp/platform-ready ]'] }
periodSeconds: 5
failureThreshold: 36
readinessProbe:
exec: { command: [sh, -c, 'curl -sf http://localhost:3000/api/health && [ -f /tmp/platform-ready ]'] }
periodSeconds: 5
resources:
requests: { cpu: 250m, memory: 512Mi }
volumes:
- name: config-data
persistentVolumeClaim: { claimName: config-data }
---
apiVersion: v1
kind: Service
metadata: { name: bgutil-provider, namespace: tale }
spec:
selector: { app: bgutil-provider }
ports: [{ name: http, port: 4416, targetPort: 4416 }]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: bgutil-provider, namespace: tale }
spec:
replicas: 1
selector: { matchLabels: { app: bgutil-provider } }
template:
metadata: { labels: { app: bgutil-provider } }
spec:
enableServiceLinks: false
automountServiceAccountToken: false
containers:
- name: provider
image: brainicism/bgutil-ytdlp-pot-provider:1.3.1
ports: [{ name: http, containerPort: 4416 }]
readinessProbe:
tcpSocket: { port: 4416 }
periodSeconds: 30
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { memory: 512Mi }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: tale-backend-egress, namespace: tale }
spec:
podSelector:
matchLabels: { tale.tier: backend }
policyTypes: [Egress]
egress:
- to:
- podSelector: {}
- to:
- namespaceSelector: {}
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.0.0/16
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
```
The platform containers start as root, fix the ownership of `/app/data`, and drop to the application user; do not set `runAsNonRoot` on them.
## Expose the proxy
The proxy is the only public service. It binds `hostPort` 80 and 443 on the node it runs on, so point the public name at that node's address. Caddy listens on the port named in `SITE_URL`: with `https://tale.example.com:8443` the pod must expose 8443 instead of 443, while port 80 keeps serving the redirect to HTTPS. The strategy is `Recreate`: two proxy Pods cannot share one `hostPort` or one `ReadWriteOnce` volume.
```yaml
# 30-proxy.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: caddy-data, namespace: tale }
spec:
accessModes: [ReadWriteOnce]
resources: { requests: { storage: 1Gi } }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: proxy, namespace: tale }
spec:
replicas: 1
strategy: { type: Recreate }
selector: { matchLabels: { app: proxy } }
template:
metadata: { labels: { app: proxy } }
spec:
enableServiceLinks: false
containers:
- name: caddy
image: ghcr.io/tale-project/tale/tale-proxy:${VERSION}
envFrom: [{ secretRef: { name: tale-env } }]
env:
- { name: BACKEND_UPSTREAM, value: 'backend-api:3005' }
- { name: OBJECT_STORE_UPSTREAM, value: 'object-store:9000' }
ports:
- { name: http, containerPort: 80, hostPort: 80 }
- { name: https, containerPort: 443, hostPort: 443 }
volumeMounts:
- { name: caddy-data, mountPath: /data }
- { name: caddy-config, mountPath: /config }
readinessProbe:
httpGet: { path: /health, port: 2020 }
periodSeconds: 10
resources:
requests: { cpu: 50m, memory: 64Mi }
volumes:
- name: caddy-data
persistentVolumeClaim: { claimName: caddy-data }
- name: caddy-config
emptyDir: {}
```
Two alternatives keep the same Pod:
- A LoadBalancer Service on 80 and 443 in front of the proxy instead of the `hostPort` entries. `TLS_MODE=selfsigned` and `letsencrypt` work unchanged; the proxy also serves `docs.` and obtains that certificate.
- An Ingress that terminates TLS. Set `TLS_MODE=external` and `TRUSTED_PROXIES` to the Ingress address range so forwarded headers are accepted, as described in [TLS and domains](https://docs.tale.dev/self-hosted/configuration/tls-and-domains).
## Run the sandbox tier
The egress proxy needs the capability set from the Compose contract and no sysctls: the entrypoint installs the IPv6 firewall with ip6tables when the node kernel offers it, and otherwise disables IPv6 in its own network namespace. A cluster that denies both blocks the Pod at start; allow the `net.ipv6.conf.*` sysctls on the kubelet in that case. The spawner creates session Pods, Secrets, and workspace claims through the Kubernetes API, so it runs with a namespaced Role and no Docker socket.
The proxy serves `SANDBOX_EGRESS_MAX_CLIENTS` connections at once (2000 by default) for every session together, each with a thread and two open files. A Pod spec cannot set process or open-file limits: at start the proxy raises its open-file limit to what its connections need, as far as the hard limit of the container runtime allows, and warns in its log when that is too low. Its threads count against the kubelet's `podPidsLimit`; on nodes that set one, keep it above the connection limit, or lower `SANDBOX_EGRESS_MAX_CLIENTS` to fit.
```yaml
# 40-sandbox.yaml
apiVersion: v1
kind: Service
metadata: { name: sandbox-egress, namespace: tale }
spec:
selector: { app: sandbox-egress }
ports: [{ name: proxy, port: 3128, targetPort: 3128 }]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: sandbox-egress, namespace: tale }
spec:
replicas: 1
selector: { matchLabels: { app: sandbox-egress } }
template:
metadata: { labels: { app: sandbox-egress } }
spec:
enableServiceLinks: false
automountServiceAccountToken: false
containers:
- name: egress
image: ghcr.io/tale-project/tale/tale-sandbox-egress:${VERSION}
env:
- { name: SANDBOX_EGRESS_MAX_CLIENTS, value: '2000' }
securityContext:
runAsUser: 0
capabilities:
drop: ['ALL']
add: ['NET_ADMIN', 'DAC_OVERRIDE', 'CHOWN', 'SETUID', 'SETGID', 'NET_BIND_SERVICE', 'KILL']
ports: [{ name: proxy, containerPort: 3128 }]
readinessProbe:
exec: { command: [sh, -c, "curl -sS -o /dev/null --max-time 3 --noproxy '*' http://127.0.0.1:3128/"] }
periodSeconds: 10
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 512Mi }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: llm-gateway-data, namespace: tale }
spec:
accessModes: [ReadWriteOnce]
resources: { requests: { storage: 1Gi } }
---
apiVersion: v1
kind: Service
metadata: { name: sandbox-llm-gateway, namespace: tale }
spec:
selector: { app: sandbox-llm-gateway }
ports: [{ name: http, port: 8080, targetPort: 8080 }]
---
apiVersion: v1
kind: Service
metadata: { name: llm-gateway, namespace: tale }
spec:
selector: { app: sandbox-llm-gateway }
ports: [{ name: http, port: 8080, targetPort: 8080 }]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: sandbox-llm-gateway, namespace: tale }
spec:
replicas: 1
strategy: { type: Recreate }
selector: { matchLabels: { app: sandbox-llm-gateway } }
template:
metadata: { labels: { app: sandbox-llm-gateway } }
spec:
enableServiceLinks: false
automountServiceAccountToken: false
securityContext: { fsGroup: 1000 }
containers:
- name: gateway
image: ghcr.io/tale-project/tale/tale-sandbox-llm-gateway:${VERSION}
envFrom: [{ secretRef: { name: tale-env } }]
ports: [{ name: http, containerPort: 8080 }]
volumeMounts: [{ name: data, mountPath: /app/data }]
readinessProbe:
httpGet: { path: /health, port: 8080 }
periodSeconds: 10
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { memory: 512Mi }
volumes:
- name: data
persistentVolumeClaim: { claimName: llm-gateway-data }
---
apiVersion: v1
kind: ServiceAccount
metadata: { name: tale-sandbox-spawner, namespace: tale }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: tale-sandbox-spawner, namespace: tale }
rules:
- apiGroups: ['']
resources: ['pods']
verbs: ['create', 'get', 'list', 'delete', 'patch']
- apiGroups: ['']
resources: ['secrets']
verbs: ['create', 'delete', 'list']
- apiGroups: ['']
resources: ['persistentvolumeclaims']
verbs: ['get', 'list', 'create', 'delete']
- apiGroups: ['networking.k8s.io']
resources: ['networkpolicies']
verbs: ['create', 'update']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: tale-sandbox-spawner, namespace: tale }
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: Role, name: tale-sandbox-spawner }
subjects: [{ kind: ServiceAccount, name: tale-sandbox-spawner, namespace: tale }]
---
apiVersion: v1
kind: Service
metadata: { name: sandbox, namespace: tale }
spec:
selector: { app: sandbox }
ports: [{ name: http, port: 8003, targetPort: 8003 }]
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: sandbox, namespace: tale }
spec:
replicas: 1
selector: { matchLabels: { app: sandbox } }
template:
metadata: { labels: { app: sandbox } }
spec:
enableServiceLinks: false
serviceAccountName: tale-sandbox-spawner
terminationGracePeriodSeconds: 30
containers:
- name: spawner
image: ghcr.io/tale-project/tale/tale-sandbox:${VERSION}
envFrom: [{ secretRef: { name: tale-env } }]
env:
- { name: SANDBOX_BACKEND, value: kubernetes }
- { name: SANDBOX_K8S_NAMESPACE, value: tale }
- { name: SANDBOX_RUNTIME_IMAGE, value: 'ghcr.io/tale-project/tale/tale-sandbox-runtime:${VERSION}' }
- { name: NODE_EXTRA_CA_CERTS, value: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt }
- { name: SANDBOX_RUNTIME, value: runc }
- { name: SANDBOX_DOCKER_IN_CONTAINER, value: 'false' }
- { name: SANDBOX_EGRESS_PROXY, value: 'http://sandbox-egress:3128' }
ports: [{ name: http, containerPort: 8003 }]
volumeMounts: [{ name: config-data, mountPath: /app/platform-config, readOnly: true }]
startupProbe:
httpGet: { path: /health, port: 8003 }
periodSeconds: 5
failureThreshold: 24
readinessProbe:
httpGet: { path: /health, port: 8003 }
periodSeconds: 10
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 512Mi }
volumes:
- name: config-data
persistentVolumeClaim: { claimName: config-data }
```
| Setting | Requirement |
| --- | --- |
| `SANDBOX_BACKEND` | `kubernetes`. Docker-only host paths and bridge names do not configure this backend. |
| `SANDBOX_K8S_NAMESPACE` | The namespace the session Pods, Secrets, and workspace claims are created in; the spawner's own namespace in this layout. Defaults to `tale-sandbox`. |
| `SANDBOX_RUNTIME_IMAGE` | The matching Tale sandbox runtime image, available to every node. |
| `NODE_EXTRA_CA_CERTS` | The cluster CA file, normally `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` inside the spawner. It is the only CA-trust mechanism the spawner honors; keep TLS verification on. |
| `SANDBOX_K8S_WORKSPACE_SIZE_LIMIT` | The size of each agent session's `/agent` workspace claim, default `4Gi`; also bounds a crawler render's temporary workspace, which needs no claim because it is never resumed, and the inner Docker temporary store when that is enabled. |
| `SANDBOX_K8S_CPU_REQUEST` / `SANDBOX_K8S_MEMORY_REQUEST` | What every session Pod requests from the scheduler, as Kubernetes quantities. Unset, an agent Pod requests `250m` and `512Mi` (`1Gi` with Docker inside the sandbox) and a crawler render `250m` and `512Mi`; a request never exceeds the Pod's limit. Raise them when agents on your nodes run heavier builds than this, so the scheduler does not place more sessions than a node can hold. |
| `SANDBOX_K8S_CACHE_STORAGECLASS` | The StorageClass for workspace claims; unset uses the cluster default. |
| `SANDBOX_RUNTIME` / `SANDBOX_RUNTIME_CLASS` | A supported runtime tier and, when needed, the installed RuntimeClass name. |
| `SANDBOX_EGRESS_PROXY` | The egress Service the sessions use, default `http://sandbox-egress:3128`. |
The spawner scales horizontally. Any replica resolves a session it did not create by its deterministic Pod name and adopts it, so exec, stop, and destroy work through whichever replica the Service picks. `SANDBOX_MAX_SESSIONS` counts the namespace, but simultaneous admissions on several replicas can exceed it briefly; use a ResourceQuota for a hard bound. The [sandbox Kubernetes contract](https://github.com/tale-project/tale/blob/main/services/sandbox/docs/kubernetes.md) documents the Pod shape and runtime details.
### What the spawner enforces
At start the spawner applies the NetworkPolicy `tale-sandbox-session-egress`: session Pods may reach DNS and the Pods of their own namespace and nothing else, so the cloud metadata service, the nodes, and other namespaces stay unreachable even for a process that ignores `HTTP_PROXY`. Public destinations pass through `sandbox-egress`. A missing `networkpolicies` permission is logged and does not stop the spawner; verify that the policy exists before you admit workloads.
Session Pods run the runner as uid 65534 with all capabilities dropped, a read-only root filesystem, no ServiceAccount token, and the session Secret mounted only as environment. The `/agent` workspace is a claim that survives an idle stop. An explicit destroy deletes it, and so does the workspace cleanup once its owner is removed or it stays unused past the organization's window; the cleanup finds the claims with `list`. Session operations use plain HTTP to runnerd on the Pod IP, port 8200; no `pods/exec` is involved.
The namespace-wide allowance is broader than the Compose network. From a session Pod, Postgres and the object store answer on their Service ports, although the session holds no credentials for them. Keeping those stores in another namespace would tighten this; that layout was not verified and needs its own policy for the backend roles.
For Docker inside sessions, choose an explicit `SANDBOX_DIND_INNER_POOL` outside the Pod, Service, and VPC ranges and read the [inner Docker network prerequisites](https://docs.tale.dev/self-hosted/configuration/environment-reference#sandbox-infrastructure); a Pod cannot discover every cluster network.
## Roll out and verify
After the apply loop, wait for every Pod to become ready and check the signals that matter:
```bash
kubectl -n tale get pods
kubectl -n tale logs -l 'app in (backend-api,backend-worker)' --tail=-1 | grep -c 'applying app migration'
kubectl -n tale get networkpolicy tale-sandbox-session-egress tale-backend-egress
curl -s https://tale.example.com/api/health
```
Whichever backend role boots first applies the migrations under an advisory lock, so the count comes from both roles together and the API log ends with `api listening on :3005`; the health endpoint answers `{"status":"ok","version":"0.5.31"}`. Then open the site, [create the first owner](https://docs.tale.dev/self-hosted/install/first-admin), and connect a provider.
Under **Settings > Sandboxes**, the deployment card carries the namespace scope in its title and shows no host CPU or memory figures; that is expected on this backend. Assign a task to an agent and wait for its deliverable: the run creates a session Pod named `tale-sbx-ses-` in the namespace, together with a `-spec` Secret and a `-ws` claim.
Prove the fence from a live session Pod:
```bash
POD=$(kubectl -n tale get pods -l tale.sandbox/role=session -o name | head -1)
kubectl -n tale exec $POD -c runner -- curl -m 5 http://169.254.169.254/
kubectl -n tale exec $POD -c runner -- curl -m 20 -s -o /dev/null -w '%{http_code}\n' https://example.com/
```
The first command times out; the second prints `200`, reached through the egress proxy. Then test the lifecycle the way an operator would rely on it: a runner restart keeps the Pod and the workspace, an idle session stops after `SANDBOX_SESSION_MAX_IDLE_MS` and leaves its claim behind, the next task resumes it with the files intact, and destroying it removes the Pod, the Secret, and the claim. With two spawner replicas, repeat a task after scaling and confirm that the second replica serves it.
Roll the API without a gap by keeping two replicas and restarting the Deployment:
```bash
kubectl -n tale rollout restart deploy/backend-api
kubectl -n tale rollout status deploy/backend-api
```
Migrations run at boot under an advisory lock while the previous image keeps serving, so the health endpoint stays green through the roll. Pin one `VERSION` across all Tale images and follow [Upgrade and recover a deployment](https://docs.tale.dev/self-hosted/operate/upgrades) before changing it; a release that changes the proxy image needs the proxy Pod recreated too.
The CLI's snapshots, blue-green flips, and rollback checks do not run on Kubernetes. Back up the claims with your storage provider's snapshots and keep the Secret, the encryption key, and the organization configuration with them; [Backups and restore](https://docs.tale.dev/self-hosted/operate/backups-and-restore) lists what a restore needs.
## Verified scope
These five files were applied unchanged, apart from the Secret values, to a fresh single-node kind cluster with Kubernetes 1.36, kube-network-policies, the local-path StorageClass, and Tale 0.5.31: boot and migrations, the public edge, the first-owner setup and the Sandboxes card, an agent task with a deliverable, the session lifecycle including idle stop, resume, and cross-replica access, the fence probes above, and a rolling restart of the API with two replicas. Multi-node scheduling with `ReadWriteMany` configuration storage, Docker inside sessions on a sysbox or kata RuntimeClass, an Ingress with `TLS_MODE=external`, and highly available stores were not part of that run.
# Run Compose yourself
Source: https://docs.tale.dev/self-hosted/install/own-compose
Use this reference when your team maintains the deployment files and rollout process. The [CLI quickstart](https://docs.tale.dev/self-hosted/install/quickstart) is the shorter path when you want Tale to generate those files and coordinate upgrades. There is no official Helm chart. The sandbox spawner supports Docker and Kubernetes backends; choose the matching runtime and network configuration.
This is a deployment contract with an application-tier example, not a complete Compose file ready to start. Assemble and validate the storage, proxy, and sandbox services before using that example.
## Choose the service layout
| Group | Services | Lifecycle |
| --- | --- | --- |
| Replicable application | `platform`, `backend-api`, `backend-worker` | Same image and release. Keep shared wire contracts compatible during replacement. |
| Persistent stores and edge | `db`, `object-store`, `proxy` | Preserve volumes, credentials, certificates, and stable network names when replacing containers. |
| Shared execution services | `sandbox`, `sandbox-egress`, `sandbox-llm-gateway` | Coordinate active sessions before replacement; the spawner needs the Docker daemon and matching workspace paths. |
| Optional video support | `bgutil-provider` | Best-effort token provider; its failure can affect video retrieval. |
The packaged layout keeps `tale_app` and `tale_knowledge` in one Postgres service with a `knowledge-db` alias. Separate database services or managed databases are also possible; configure their connections and backup coverage explicitly. Recreating a stateful container does not inherently destroy its data, but removing or replacing its volume can.
## Pin compatible images
Set `VERSION` in Compose's `.env` to the Tale release you have reviewed and tested. Export that same value in your shell when running the separate runtime-image pull below. Keep Tale images on one release; the two upstream services have their own pinned versions.
| Service | Image |
| --- | --- |
| `platform`, `backend-api`, `backend-worker` | `ghcr.io/tale-project/tale/tale-platform:` |
| `proxy` | `ghcr.io/tale-project/tale/tale-proxy:` |
| `db` | `ghcr.io/tale-project/tale/tale-db:` |
| `sandbox` | `ghcr.io/tale-project/tale/tale-sandbox: