# Polimorf Documentation
Full text of the Polimorf developer docs. Polimorf is an AI application platform: integrate once, evolve AI from the dashboard.
# Introduction
Polimorf is an AI application platform. Developers integrate once; product teams evolve the AI continuously.
Source: https://docs.polimorf.app/docs
Polimorf is an **AI application platform**. You integrate it once — through a
typed SDK or a plain HTTPS API — and your product team evolves prompts, models,
knowledge, and tools from a dashboard, without you redeploying your application.
## Two ways to run a generation
Polimorf's public runtime gives you two entry points, depending on where the
configuration lives:
The **deployed assistant** path is the one that delivers the platform's promise:
your code names an assistant and passes input; your product team changes the
model or rewrites the prompt by publishing a new version, and your running
application picks it up with no code change and no redeploy.
## What you get
- **Provider-neutral runtime.** One contract in front of OpenAI, Anthropic, and
more. Switch providers or models without touching integration code.
- **Streaming.** Token-by-token Server-Sent Events for both ad-hoc generations
and deployed assistants. See [Streaming](/docs/streaming).
- **Typed SDK.** [`@polimorfapp/sdk`](https://www.npmjs.com/package/@polimorfapp/sdk)
handles auth, timeouts, cancellation, and a typed error catalog.
- **Bring your own keys.** Generations run against your workspace's own provider
credentials.
## Start here
---
# Quickstart
Install the Polimorf SDK, authenticate with an API key, and run your first generation — streaming and non-streaming.
Source: https://docs.polimorf.app/docs/quickstart
This guide takes you from nothing to a running generation. It uses the official
TypeScript SDK, [`@polimorfapp/sdk`](https://www.npmjs.com/package/@polimorfapp/sdk);
every call is also a plain HTTPS request you can make from any language (see the
[API Reference](/docs/api)).
## Prerequisites
- **Node.js 20 or newer** — the SDK uses the built-in global `fetch`.
- **An API key.** Create one in your Polimorf dashboard. Keys look like
`csk_live_…` and carry the `runtime:execute` scope by default. See
[Authentication](/docs/authentication) for details.
- **A provider credential configured on your workspace** (e.g. an OpenAI key),
set in the dashboard — generations run against your own provider account.
## 1. Install the SDK
```bash
npm install @polimorfapp/sdk
```
## 2. Configure credentials
The SDK reads your API key from the `POLIMORF_API_KEY` environment variable, so
you never hard-code it:
```bash
export POLIMORF_API_KEY="csk_live_your_key_here"
```
By default the client talks to the managed API at `https://api.polimorf.app`. If
you self-host, set `POLIMORF_BASE_URL` (or pass `baseUrl` explicitly).
## 3. Run your first generation
There are two ways to run a generation. Start with whichever fits your setup.
### Option A — Run a deployed assistant (recommended)
If your team has already published an assistant in the dashboard, call it by its
**slug**. You send only the input; the provider, model, and prompt come from the
published version — so your team can change them later without a code change.
```ts title="run-assistant.ts"
import { createClient } from '@polimorfapp/sdk';
// apiKey and baseUrl are read from the environment when omitted.
const client = createClient();
const result = await client.assistant('support').run({
input: 'How do I cancel my subscription?',
});
console.log(result.message.content);
console.log('finish:', result.finishReason);
console.log('tokens:', result.usage.totalTokens);
```
You can pass values for the version's declared prompt variables, and target a
non-default environment:
```ts
const result = await client.assistant('support').run({
input: 'How do I cancel my subscription?',
variables: { plan: 'pro', locale: 'en-US' },
environment: 'production', // defaults to "production"
});
```
### Option B — Run an ad-hoc generation
If you want full control from the client, send the provider, model, and messages
yourself:
```ts title="run-execute.ts"
import { createClient } from '@polimorfapp/sdk';
const client = createClient();
const result = await client.runtime.execute({
providerName: 'openai',
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'You are a concise assistant.' },
{ role: 'user', content: 'Explain what an embedding is in one sentence.' },
],
config: { temperature: 0.2, maxOutputTokens: 200 },
});
console.log(result.message.content);
```
## 4. Stream the response
For a typing-indicator experience, stream tokens as they are produced. Both
`runtime.stream(...)` and `assistant(slug).stream(...)` return an async iterable
of typed events — consume it with `for await`:
```ts title="stream.ts"
import { createClient } from '@polimorfapp/sdk';
const client = createClient();
for await (const event of client.assistant('support').stream({
input: 'Give me three tips for onboarding.',
})) {
switch (event.type) {
case 'delta':
process.stdout.write(event.content);
break;
case 'usage':
console.log('\ntokens:', event.usage.totalTokens);
break;
case 'done':
console.log('\nfinished:', event.finishReason);
break;
case 'error':
console.error('\nprovider error:', event.error.message);
break;
}
}
```
A provider failure arrives as a terminal `error` **event**, not a thrown
exception — only a transport failure (network, timeout, abort) throws. See
[Streaming](/docs/streaming) for the full event contract.
## 5. Handle errors
Any non-2xx response is thrown as a typed `PolimorfApiError` subclass you can
branch on:
```ts
import {
createClient,
AuthenticationError,
RateLimitError,
} from '@polimorfapp/sdk';
const client = createClient();
try {
const result = await client.runtime.execute({
providerName: 'openai',
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello' }],
});
console.log(result.message.content);
} catch (err) {
if (err instanceof AuthenticationError) {
// 401 — bad or missing API key
} else if (err instanceof RateLimitError) {
// 429 — back off and retry
} else {
throw err;
}
}
```
The full catalog of error codes and status mappings is in
[Errors](/docs/errors).
## Next steps
---
# Authentication
Authenticate to the Polimorf runtime API with a workspace API key sent as a bearer token, and manage scopes safely.
Source: https://docs.polimorf.app/docs/authentication
The public runtime API authenticates with a **workspace API key**, not a user
session. Every request carries the key as a bearer token.
## API keys
- A key belongs to a **workspace** and is created and revoked from the Polimorf
dashboard (or the API-keys admin API).
- Keys are prefixed `csk_live_` followed by a random secret, e.g.
`csk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`.
- The full secret is shown **once**, at creation time. Store it in a secret
manager or environment variable — Polimorf keeps only a hash and cannot show
it again.
Treat an API key like a password. Never commit it to source control, embed it
in a browser bundle, or expose it to end users. Call Polimorf from your
server, not from client-side code.
## Sending the key
Send the key in the `Authorization` header as a bearer token on every request:
```http
POST /runtime/execute HTTP/1.1
Host: api.polimorf.app
Authorization: Bearer csk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
```
### With the SDK
The SDK attaches the header for you. Provide the key explicitly or via the
`POLIMORF_API_KEY` environment variable — an explicit option always wins:
```ts
import { createClient } from '@polimorfapp/sdk';
// Read from POLIMORF_API_KEY in the environment (recommended):
const client = createClient();
// …or pass it explicitly:
const explicit = createClient({ apiKey: 'csk_live_…' });
```
`createClient()` throws immediately if no key is configured, so a
misconfiguration fails fast at startup rather than on the first request.
### With curl
```bash
curl https://api.polimorf.app/runtime/execute \
-H "Authorization: Bearer $POLIMORF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"providerName": "openai",
"model": "gpt-4o",
"messages": [{ "role": "user", "content": "Hello" }]
}'
```
## Scopes
Each key carries a set of **scopes**, and an endpoint requires a specific one:
| Scope | Grants |
| ----------------- | -------------------------------------------------------------------------------------- |
| `runtime:execute` | Run deployed assistants and raw runtime executions (`/runtime/*`). Granted by default. |
| `knowledge:read` | List knowledge bases and their documents (`GET /knowledge/*`). |
| `knowledge:write` | Create/delete bases, upload and delete documents (`POST`/`DELETE /knowledge/*`). |
New keys are granted `runtime:execute` by default; `knowledge:read`/
`knowledge:write` are **opt-in** — request them when creating the key. A call
whose key lacks the required scope is rejected with `403` (`FORBIDDEN`).
Knowledge management is exposed on the SDK as `client.knowledge`
(`createSource`, `listSources`, `uploadDocument`, `listDocuments`,
`deleteDocument`, `deleteSource`); `uploadDocument` runs the presigned
upload for you.
## Base URL
| Environment | Base URL |
| ----------------- | -------------------------- |
| Managed (default) | `https://api.polimorf.app` |
| Self-hosted | your own deployment |
The SDK defaults to the managed URL. Point it elsewhere with the `baseUrl`
option or the `POLIMORF_BASE_URL` environment variable:
```ts
const client = createClient({ baseUrl: 'https://api.your-company.internal' });
```
## Authentication failures
| Status | Code | Meaning |
| ------ | --------------------- | ---------------------------------------------------------------------- |
| `401` | `UNAUTHENTICATED` | Missing, malformed, or empty `Authorization` header. |
| `401` | `INVALID_CREDENTIALS` | The key is unknown, revoked, or does not carry the `csk_live_` prefix. |
| `403` | `FORBIDDEN` | The key is valid but lacks the required scope. |
With the SDK, a `401` is thrown as an `AuthenticationError` and a `403` as a
`PermissionDeniedError`. See [Errors](/docs/errors) for the full catalog.
## Rotating a key
1. Create a new key in the dashboard.
2. Roll it out to your environment (`POLIMORF_API_KEY`).
3. Revoke the old key. Revocation takes effect immediately — subsequent requests
with the old key fail with `401` `INVALID_CREDENTIALS`.
---
# Connect via MCP
Drive your Polimorf workspace from an MCP client such as Claude, authenticated with OAuth 2.1 and scoped to your own role and organization.
Source: https://docs.polimorf.app/docs/mcp
Polimorf exposes its control plane over the **Model Context Protocol (MCP)**, so
an MCP client — Claude Desktop, the Claude web connectors, or your own agent —
can read and manage your assistants, environments, knowledge, deployments and
more, acting **as you**. Every call reuses the same role-based permissions as the
dashboard: an MCP session can never do anything your account could not.
Unlike the runtime API (which uses a workspace [API key](/docs/authentication)),
the control plane authenticates with **OAuth 2.1**. You approve a connection once
on a consent screen; the client receives short-lived tokens and you can revoke
the connection at any time.
## The MCP endpoint
| Environment | MCP endpoint |
| ----------------- | ------------------------------ |
| Managed (default) | `https://api.polimorf.app/mcp` |
| Self-hosted | `https:///mcp` |
The endpoint speaks Streamable-HTTP JSON-RPC. Authorization metadata is published
for automatic discovery (no manual client secret):
- `GET /.well-known/oauth-protected-resource` (RFC 9728)
- `GET /.well-known/oauth-authorization-server` (RFC 8414)
- `GET /.well-known/jwks.json` — the public keys that sign access tokens
## Connect Polimorf
Most MCP clients support adding a remote server by URL and will walk you through
the OAuth flow automatically.
### Claude (Desktop or web connectors)
Add a custom connector pointing at the MCP endpoint:
```json
{
"mcpServers": {
"polimorf": {
"url": "https://api.polimorf.app/mcp"
}
}
}
```
When you enable the connector, Claude:
1. Discovers the authorization server from the endpoint's metadata.
2. Registers itself dynamically (RFC 7591) — no manual client id/secret.
3. Sends you to Polimorf's **consent screen**, where you sign in (if needed),
pick which organization to grant access to, and approve the requested scopes.
4. Exchanges the authorization code (PKCE) for an access token and starts a
session.
Access is **single-organization by default**. On the consent screen you choose
exactly one organization to bind the connection to; the session can never be
steered into another organization you belong to. Choosing "all organizations"
is an explicit opt-in.
## Scopes
A connection carries one or more MCP scopes. They gate whole classes of tools,
and are intersected with your role's permissions — you need **both** the scope
and the underlying permission for a tool to run.
| Scope | Grants |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mcp:read` | Read organizations, assistants, versions, environments, knowledge, traces (default) |
| `mcp:write` | Create/update assistants and their drafts; publish versions; create/update environments and knowledge; author evaluation datasets; publish, roll back and run evaluations; run the playground; execute deployed assistants |
| `mcp:admin` | Administer members, API keys and provider credentials |
A freshly approved connection defaults to **read-only** (`mcp:read`). Destructive
tools are flagged to the client so it can confirm before running them.
Organization owners can disable administration over MCP entirely. When that
toggle is off, every `mcp:admin` tool is denied for the organization
regardless of the caller's role.
## What you can do
Once connected, the client can list and call Polimorf tools. A sample of the
catalog:
- **Discovery** — `whoami`, `list_organizations`, `list_workspaces`
- **Assistants** — `list_assistants`, `get_assistant`, `create_assistant`,
`update_assistant`, `archive_assistant`, `restore_assistant`
- **Draft editing** — `get_assistant_draft`, `update_assistant_draft` (edit the
prompt blocks, prompt variables, model configuration and knowledge/tool
bindings of the working draft)
- **Versions & deployments** — `list_assistant_versions`, `get_assistant_version`,
`create_assistant_version` (publish the current draft), `list_deployments`,
`publish_deployment`, `rollback_deployment`
- **Environments** — `list_environments`, `create_environment`,
`update_environment`, `delete_environment`
- **Knowledge** — `list_knowledge_sources`, `list_knowledge_documents`,
`search_knowledge` (semantic search over a base's content),
`download_knowledge_document` (fetch a document's original content — text
files come back as text, binary files base64-encoded),
`create_knowledge_source`, `add_knowledge_text` (add an inline text/markdown
document; binary files use the API/SDK upload), `delete_knowledge_source`,
`delete_knowledge_document`
- **Evaluations** — `run_evaluation`, `list_evaluation_runs`, `get_evaluation_run`,
`list_evaluation_datasets`, `get_evaluation_dataset`, `create_evaluation_dataset`,
`delete_evaluation_dataset`, `add_evaluation_case`, `delete_evaluation_case`
- **Runtime** — `execute_assistant` (invoke a deployed assistant by slug),
`run_playground` (run a single turn against a draft or pinned version to test
prompt/model/tool changes before publishing)
- **Admin** (`mcp:admin`) — `list_members`, `update_member_role`,
`remove_member`, `list_api_keys`, `create_api_key`, `revoke_api_key`,
`list_provider_credentials`, `set_provider_credential`,
`delete_provider_credential`
Tools that operate on a single organization accept a `workspaceId` (and other
ids) as arguments; for an all-organizations connection they also take an
`organizationId`.
## Managing connections
Every approved connection appears in the dashboard under **Settings → Connected
applications**, where you can review its scopes and organization and revoke it.
Revocation is immediate: outstanding access tokens stop working on the next
request, and the refresh token is invalidated too.
## Security model
- **OAuth 2.1** with PKCE, exact redirect-URI matching, and refresh-token
rotation with reuse detection.
- **Asymmetric, short-lived access tokens** (published JWKS); the resource server
re-checks the grant on every request, so revoking a connection takes effect
immediately.
- **Least privilege** — deny-by-default on the intersection of the token scope
and your role's permission; every tool call is audited.
---
# Streaming
Stream a Polimorf generation token-by-token over Server-Sent Events, with a typed event contract and cancellation.
Source: https://docs.polimorf.app/docs/streaming
Streaming lets you render a response as it is produced instead of waiting for the
whole generation. Every streaming endpoint returns
[Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events);
the SDK decodes them into typed events.
## The event sequence
A stream emits incremental content, then a final usage count and a done marker —
or a terminal error:
```
delta → delta → … → usage → done
```
| Event | Payload | Meaning |
| ------- | -------------------------------------------------------- | -------------------------------------------------- |
| `delta` | `{ content: string }` | A chunk of generated text. Concatenate in order. |
| `usage` | `{ usage: { inputTokens, outputTokens, totalTokens } }` | Final token accounting. |
| `done` | `{ finishReason }` | The generation finished. See finish reasons below. |
| `error` | `{ error: { code, message, retryable, providerName? } }` | A terminal provider error; the stream ends. |
### Finish reasons
`stop` · `length` · `tool_calls` · `content_filter` · `error`
## Consuming a stream with the SDK
Both `client.runtime.stream(...)` and `client.assistant(slug).stream(...)`
return an `AsyncIterable` of typed events. Iterate with `for await`:
```ts
import { createClient } from '@polimorfapp/sdk';
const client = createClient();
const stream = client.runtime.stream({
providerName: 'openai',
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Write a haiku about the sea.' }],
});
let text = '';
for await (const event of stream) {
if (event.type === 'delta') {
text += event.content;
process.stdout.write(event.content);
} else if (event.type === 'done') {
console.log('\nfinish:', event.finishReason);
} else if (event.type === 'error') {
console.error('\nprovider error:', event.error.code, event.error.message);
}
}
```
A provider failure is delivered as a terminal `error` **event**, mirroring the
wire contract — it is not thrown. Only a transport failure (network, timeout,
abort) throws a `PolimorfError`. Handle both: branch on the `error` event and
wrap the loop in `try/catch`.
## Cancellation
Pass an `AbortSignal` to stop a stream in flight — for example when a user
navigates away or a request times out:
```ts
const controller = new AbortController();
// Abort after 5 seconds.
setTimeout(() => controller.abort(), 5_000);
for await (const event of client
.assistant('support')
.stream(
{ input: 'Summarize our refund policy.' },
{ signal: controller.signal },
)) {
if (event.type === 'delta') process.stdout.write(event.content);
}
```
The caller signal is composed with the client's own request timeout, so
whichever fires first ends the stream.
## Raw SSE (without the SDK)
The wire format is standard SSE — each frame has an `event:` name and a JSON
`data:` payload:
```
event: delta
data: {"content":"Hello"}
event: delta
data: {"content":", world"}
event: usage
data: {"inputTokens":12,"outputTokens":3,"totalTokens":15}
event: done
data: {"finishReason":"stop"}
```
Unrecognized event names should be ignored, so a future server-side event type
never breaks an existing client.
---
# Errors
The Polimorf error envelope, HTTP status mapping, and the typed SDK error classes you can branch on.
Source: https://docs.polimorf.app/docs/errors
Every error the API returns uses one **stable envelope**: a machine-readable
`code`, the HTTP `status`, a human `message`, and — for validation errors — a
list of offending `fields`.
## The error envelope
```json
{
"code": "VALIDATION_ERROR",
"status": 400,
"message": "messages must contain at least one message",
"fields": [{ "field": "messages", "issue": "must not be empty" }]
}
```
- **`code`** is the value to branch on in your application. It is drawn from a
closed catalog and is stable across releases.
- **`message`** is for humans (logs, debugging) — do not match on it.
- **`fields`** is present only for `VALIDATION_ERROR` (`400`).
## Status mapping
The SDK throws the most specific `PolimorfApiError` subclass for each status, so
you can `catch` with `instanceof`:
| Status | SDK class | Typical codes |
| ------ | ----------------------- | --------------------------------------------- |
| `400` | `BadRequestError` | `VALIDATION_ERROR`, `INVALID_REQUEST` |
| `401` | `AuthenticationError` | `UNAUTHENTICATED`, `INVALID_CREDENTIALS` |
| `403` | `PermissionDeniedError` | `FORBIDDEN` |
| `404` | `NotFoundError` | `ASSISTANT_NOT_FOUND`, `DEPLOYMENT_NOT_FOUND` |
| `409` | `ConflictError` | `IDEMPOTENCY_KEY_CONFLICT` |
| `429` | `RateLimitError` | `RATE_LIMITED`, `COST_QUOTA_EXCEEDED` |
| `≥500` | `ServerError` | `INTERNAL_ERROR` |
An unmapped status is thrown as the base `PolimorfApiError`, so an unforeseen
status never produces an unexpected shape.
## Handling errors
```ts
import {
createClient,
PolimorfApiError,
PolimorfError,
BadRequestError,
RateLimitError,
} from '@polimorfapp/sdk';
const client = createClient();
try {
await client.assistant('support').run({ input: 'Hello' });
} catch (err) {
if (err instanceof BadRequestError) {
// Inspect err.fields for the offending inputs.
console.error(err.fields);
} else if (err instanceof RateLimitError) {
// 429 — back off and retry.
} else if (err instanceof PolimorfApiError) {
// Any other non-2xx: err.status and err.code are set.
console.error(err.status, err.code, err.message);
} else if (err instanceof PolimorfError) {
// Transport failure: network, timeout, or abort.
console.error('transport error', err.cause);
} else {
throw err;
}
}
```
Two error families exist. **`PolimorfApiError`** means the server replied with
a non-2xx envelope (it has `status`, `code`, and sometimes `fields`). Its
base, **`PolimorfError`**, is thrown when the request never completed — a
network failure, timeout, or abort. Catch `PolimorfError` to cover both.
## Switching on the code
For finer control than status classes, switch on the stable `code`. Unknown
codes stay typed as `string`, so a newly published server code never breaks an
older SDK build:
```ts
if (err instanceof PolimorfApiError) {
switch (err.code) {
case 'COST_QUOTA_EXCEEDED':
// Workspace spend limit reached.
break;
case 'INVALID_CREDENTIALS':
// Bad or revoked API key.
break;
default:
// Handle or rethrow.
}
}
```
## Error code catalog
The codes most relevant to the runtime API:
| Code | Meaning |
| -------------------------- | -------------------------------------------------------- |
| `VALIDATION_ERROR` | Request body failed validation; see `fields`. |
| `INVALID_REQUEST` | Request was malformed or semantically invalid. |
| `PAYLOAD_TOO_LARGE` | Request body exceeded the size limit. |
| `UNAUTHENTICATED` | Missing or malformed credentials. |
| `INVALID_CREDENTIALS` | Unknown or revoked API key. |
| `FORBIDDEN` | Key lacks the required scope. |
| `RATE_LIMITED` | Too many requests; retry after backoff. |
| `COST_QUOTA_EXCEEDED` | Workspace spend quota reached. |
| `ASSISTANT_NOT_FOUND` | No assistant matches the slug. |
| `DEPLOYMENT_NOT_FOUND` | The assistant is not deployed to the target environment. |
| `IDEMPOTENCY_KEY_CONFLICT` | An idempotency key was reused with a different body. |
| `INTERNAL_ERROR` | Unexpected server error; safe to retry. |
The catalog is a closed set; the SDK vendors it as the `ErrorCode` union for
autocomplete while still accepting any string for forward compatibility.
---
# AI Assistants
Use Polimorf from Cursor, Claude Code, Copilot, and any AI coding assistant — llms.txt, per-page Markdown, and a drop-in rules file.
Source: https://docs.polimorf.app/docs/ai-assistants
Polimorf is built to be integrated _through_ an AI coding assistant. Point your
assistant at the resources below and it will know how to install the SDK,
authenticate, and call the API correctly — without you copy-pasting docs by hand.
## Drop-in rules file
Add [`AGENTS.md`](/AGENTS.md) to your repository. It teaches any assistant the
Polimorf conventions: the `@polimorfapp/sdk` package, the `createClient()`
pattern, running a deployed assistant by slug, streaming, and error handling.
- **Cursor** — save it as `AGENTS.md`, or paste its contents into `.cursorrules`.
- **Claude Code** — paste into your project's `CLAUDE.md`.
- **GitHub Copilot** — paste into `.github/copilot-instructions.md`.
```bash
curl -o AGENTS.md https://docs.polimorf.app/AGENTS.md
```
Your assistant stops guessing API shapes. It uses `client.assistant(slug)` by
default, reads the key from the environment, and handles streaming `error`
events correctly — because the rules file says so.
## llms.txt
The whole documentation is published in the [llms.txt](https://llmstxt.org)
format, so an assistant can load full product context in one fetch:
- [`/llms.txt`](/llms.txt) — an index of every page with links and summaries.
- [`/llms-full.txt`](/llms-full.txt) — the entire docs as a single Markdown file.
Paste either URL into your assistant's chat, or reference it from your rules
file. When it needs details, it fetches the source instead of hallucinating.
## Per-page Markdown
Every docs page has a toolbar at the top with:
- **Copy for LLM** — copies that page as clean Markdown to your clipboard.
- **View as Markdown** — opens the raw Markdown (also fetchable by an assistant).
- **Open in ChatGPT / Claude** — starts a chat pre-loaded with the page URL.
Any page's raw Markdown lives at `/raw` + its path — for example
[`/raw/docs/quickstart`](/raw/docs/quickstart).
## Next steps
---
# Knowledge bases
Create knowledge bases and upload documents from the SDK, the API, or MCP — then ground your assistants on them.
Source: https://docs.polimorf.app/docs/knowledge
A **knowledge base** is a named collection of documents Polimorf ingests
(chunks + embeds) so your assistants can ground their answers on your own
content (retrieval-augmented generation). You can manage bases and upload
documents from the dashboard, the **SDK/API**, or an **MCP** client — not just
the dashboard.
## What you can do programmatically
- Create, list, and delete knowledge bases.
- Upload documents (the SDK handles the multi-step upload for you), list them,
and delete them.
- Search a base's content semantically (via MCP, or as part of an assistant's
retrieval at runtime).
## Scopes
The knowledge endpoints are **not** covered by the default key scope. Create an
API key with the knowledge scopes (in addition to `runtime:execute` if the same
key also runs assistants):
| Scope | Grants |
| ----------------- | ------------------------------------------------- |
| `knowledge:read` | List bases and documents. |
| `knowledge:write` | Create/delete bases, upload and delete documents. |
See [Authentication](/docs/authentication) for how scopes are enforced. All
knowledge calls act within the **key's own workspace** — you never pass an
organization or workspace id.
## Upload a document (SDK)
`client.knowledge.uploadDocument` runs the whole flow for you: it requests a
presigned URL, uploads the bytes straight to object storage, and confirms
completion (which starts ingestion). A `string` body is sent as UTF-8; binary
formats (PDF, DOCX) pass a `Uint8Array`. The content type is derived from the
filename extension when you omit it.
```ts
import { createClient } from '@polimorfapp/sdk';
const client = createClient(); // reads POLIMORF_API_KEY
// 1. Create a base (or reuse one from client.knowledge.listSources()).
const source = await client.knowledge.createSource({
name: 'Product FAQ',
description: 'Answers the support assistant can ground on.',
});
// 2. Upload a document — returns immediately with status PENDING.
const doc = await client.knowledge.uploadDocument({
sourceId: source.id,
filename: 'faq.md',
content: '# FAQ\n\n## Reset password\nOpen Settings → Security.\n',
});
// 3. Ingestion (chunk + embed) runs in the background. Poll until READY:
const docs = await client.knowledge.listDocuments(source.id);
```
Uploading a binary file is the same call with `Uint8Array` content:
```ts
import { readFile } from 'node:fs/promises';
await client.knowledge.uploadDocument({
sourceId: source.id,
filename: 'handbook.pdf',
content: new Uint8Array(await readFile('./handbook.pdf')),
});
```
### Supported file types
`pdf`, `txt`, `md`/`markdown`, `html`/`htm`, `csv`, `json`, `docx`. A single
document is capped at **25 MiB**, and each workspace has a total storage
allowance.
### Ingestion status
`uploadDocument` resolves as soon as ingestion is **enqueued**; the returned
document is `PENDING`. It advances to `PROCESSING`, then `READY` (searchable) or
`FAILED`. Poll `listDocuments` to observe the transition.
## Manage bases and documents (SDK)
```ts
await client.knowledge.listSources();
await client.knowledge.deleteSource(sourceId); // also deletes its documents
await client.knowledge.listDocuments(sourceId);
await client.knowledge.deleteDocument(sourceId, documentId);
```
## From MCP
An MCP client (e.g. Claude) can manage knowledge as you: `create_knowledge_source`,
`add_knowledge_text` (add a text/markdown document inline), `list_knowledge_sources`,
`list_knowledge_documents`, `search_knowledge` (semantic search over a base), and
the delete tools. Binary files go through the SDK/API upload. See
[Connect via MCP](/docs/mcp).
## API
Every SDK call maps to a plain HTTPS request under `/knowledge/*`. See the
[Knowledge API reference](/docs/api/knowledge) for the endpoints, including the
one-shot direct upload (`POST /knowledge/sources/:sourceId/documents/upload`).
---
# Changelog
What's new in Polimorf — new capabilities, improvements, and fixes, newest first.
Source: https://docs.polimorf.app/docs/changelog
What's shipped in Polimorf, newest first. Polimorf is in early access, so
expect frequent updates. Breaking changes to the public SDK or API are called
out explicitly.
## August 2026
**Tool calling, generally available.** Assistants can now call your functions.
Define a tool once, and Polimorf runs the full call loop — streamed or buffered
— passing arguments to your handler and feeding results back to the model. See
[the API reference](/docs/api/execute).
**Bulk import & JSON authoring.** Import many assistants at once and author or
edit configuration as JSON, with validation before publish.
## July 2026
**Evaluations.** Score a version against a dataset before it reaches users —
compare versions side by side and gate risky changes on the results.
**Dashboard redesign.** A refreshed workspace with a global workspace switcher
and a cleaner, faster surface for building and shipping assistants.
## June 2026
**Polimorf.** Rebranded from the original codename to **Polimorf**. The SDK is
now published as
[`@polimorfapp/sdk`](https://www.npmjs.com/package/@polimorfapp/sdk) and the API
lives at `api.polimorf.app`.
**Google sign-in.** Log in and sign up with Google, alongside email.
## Earlier
**Knowledge & RAG.** Point an assistant at your docs, policies, and macros.
Polimorf searches them on demand and cites every source in the answer.
**Versions & environments.** Every edit is a version; promote or roll back to
any environment in one click while your app keeps calling a single stable slug.
**Traces, tokens & cost.** Every run is captured — prompt, tokens, latency, and
cost — with opt-in content capture.
**Typed SDK & streaming.** `@polimorfapp/sdk` handles auth, timeouts,
cancellation, and a typed error catalog, with token-by-token streaming over
Server-Sent Events. See [Streaming](/docs/streaming).
---
# Overview
The Polimorf public runtime API — base URL, conventions, and the runtime endpoints for ad-hoc generations and deployed assistants.
Source: https://docs.polimorf.app/docs/api
The public runtime API is the API-key-authenticated HTTP surface for running
generations. The [SDK](https://www.npmjs.com/package/@polimorfapp/sdk) is the
recommended integration path, but every operation is a plain HTTPS request you
can make from any language.
The dashboard/management API (assistants, workspaces, deployments, …) is a
separate, session-authenticated surface for the Polimorf UI and is not part of
this reference.
## Base URL
All paths are relative to your instance's base URL, with no global prefix:
```
https://api.polimorf.app
```
The execute endpoint, for example, is
`https://api.polimorf.app/runtime/execute`.
## Authentication
Send your workspace API key as a bearer token on every request:
```http
Authorization: Bearer csk_live_xxxxxxxxxxxxxxxxxxxxxxxx
```
The runtime endpoints require the `runtime:execute` scope. See
[Authentication](/docs/authentication).
## Conventions
- Request and response bodies are JSON (`Content-Type: application/json`).
- Timestamps are ISO-8601 UTC.
- Non-2xx responses use the [error envelope](/docs/errors) (`code`, `status`,
`message`, optional `fields`).
- Streaming endpoints return [Server-Sent Events](/docs/streaming).
## Endpoints
| Method & path | Purpose |
| --------------------------------------- | ------------------------------------------------------------------- |
| `POST /runtime/execute` | [Run an ad-hoc, non-streaming generation](/docs/api/execute) |
| `POST /runtime/stream` | [Stream an ad-hoc generation (SSE)](/docs/api/execute#streaming) |
| `POST /runtime/assistants/:slug` | [Run a deployed assistant by slug](/docs/api/assistants) |
| `POST /runtime/assistants/:slug/stream` | [Stream a deployed assistant (SSE)](/docs/api/assistants#streaming) |
---
# Ad-hoc generation
POST /runtime/execute and /runtime/stream — run a generation by specifying the provider, model, and messages on each request.
Source: https://docs.polimorf.app/docs/api/execute
Run a generation where the client supplies everything: the provider, the model,
and the full message list. Use this when the caller owns the configuration; for
configuration managed by your team, use a
[deployed assistant](/docs/api/assistants) instead.
## `POST /runtime/execute`
Runs one non-streaming generation and returns the complete result.
### Request body
| Field | Type | Required | Description |
| -------------------- | --------------------------------- | -------- | ---------------------------------------------- |
| `providerName` | string | yes | Target provider, e.g. `openai` or `anthropic`. |
| `model` | string | yes | Provider model identifier, e.g. `gpt-4o`. |
| `messages` | array | yes | Ordered conversation; at least one message. |
| `messages[].role` | `system` \| `user` \| `assistant` | yes | Message author role. |
| `messages[].content` | string | yes | Message text. |
| `config` | object | no | Generation settings (below); defaults to `{}`. |
#### `config`
| Field | Type | Description |
| ----------------- | ---------------- | -------------------------------- |
| `temperature` | number | Sampling temperature. |
| `maxOutputTokens` | number | Cap on generated tokens. |
| `responseFormat` | `text` \| `json` | Force plain text or JSON output. |
| `timeoutMs` | number | Per-generation timeout. |
| `maxRetries` | number | Provider retry budget. |
### Example request
```bash
curl https://api.polimorf.app/runtime/execute \
-H "Authorization: Bearer $POLIMORF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"providerName": "openai",
"model": "gpt-4o",
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "Explain embeddings in one sentence." }
],
"config": { "temperature": 0.2, "maxOutputTokens": 200 }
}'
```
### Response
```json
{
"message": { "role": "assistant", "content": "An embedding is a…" },
"finishReason": "stop",
"usage": { "inputTokens": 24, "outputTokens": 18, "totalTokens": 42 }
}
```
| Field | Type | Description |
| -------------- | ------ | --------------------------------------------------------------- |
| `message` | object | The generated message (`role`, `content`). |
| `finishReason` | string | `stop`, `length`, `tool_calls`, `content_filter`, or `error`. |
| `usage` | object | Token accounting: `inputTokens`, `outputTokens`, `totalTokens`. |
### With the SDK
```ts
const result = await client.runtime.execute({
providerName: 'openai',
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Explain embeddings in one sentence.' }],
config: { temperature: 0.2 },
});
```
## Streaming
### `POST /runtime/stream`
Identical request body to `/runtime/execute`, but the response is a
[Server-Sent Events](/docs/streaming) stream of `delta` → `usage` → `done`
events (or a terminal `error`).
```ts
for await (const event of client.runtime.stream({
providerName: 'openai',
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Write a haiku about the sea.' }],
})) {
if (event.type === 'delta') process.stdout.write(event.content);
}
```
See [Streaming](/docs/streaming) for the full event contract and cancellation.
---
# Deployed assistants
POST /runtime/assistants/:slug and /stream — run an assistant your team published, sending only the input.
Source: https://docs.polimorf.app/docs/api/assistants
Run an assistant your team has **published and deployed** in the dashboard. The
provider, model, and prompt come from the published version's frozen snapshot —
you send only the input. When your team publishes a new version, your running
application picks it up with no code change and no redeploy.
## `POST /runtime/assistants/:slug`
Runs the assistant's current deployed version and returns the complete result.
### Path parameters
| Parameter | Description |
| --------- | ------------------------------------------------ |
| `slug` | The assistant's slug, as shown in the dashboard. |
### Request body
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------ |
| `input` | string | yes | The runtime input to run the deployed version against. |
| `variables` | object | no | Values for the version's declared prompt variables. Values are string, number, or boolean. |
| `environment` | string | no | Deployment environment slug. Defaults to `production`. |
You do **not** send a provider, model, or messages here — those are fixed by
the published version. That is what lets your team change the model or rewrite
the prompt without a client change.
### Example request
```bash
curl https://api.polimorf.app/runtime/assistants/support \
-H "Authorization: Bearer $POLIMORF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "How do I cancel my subscription?",
"variables": { "plan": "pro", "locale": "en-US" },
"environment": "production"
}'
```
### Response
Identical to an [ad-hoc generation](/docs/api/execute#response):
```json
{
"message": { "role": "assistant", "content": "To cancel, open…" },
"finishReason": "stop",
"usage": { "inputTokens": 31, "outputTokens": 22, "totalTokens": 53 }
}
```
A slug that is not deployed to the target environment returns `404`
(`ASSISTANT_NOT_FOUND` or `DEPLOYMENT_NOT_FOUND`).
### With the SDK
```ts
const result = await client.assistant('support').run({
input: 'How do I cancel my subscription?',
variables: { plan: 'pro' },
});
console.log(result.message.content);
```
## Streaming
### `POST /runtime/assistants/:slug/stream`
Identical request body, streamed as [Server-Sent Events](/docs/streaming):
```ts
for await (const event of client.assistant('support').stream({
input: 'Give me three onboarding tips.',
})) {
if (event.type === 'delta') process.stdout.write(event.content);
}
```
A non-2xx response before the stream opens (for example, `404` when the slug is
not deployed) throws a `PolimorfApiError`; a terminal provider error arrives as
an `error` event. See [Streaming](/docs/streaming).
---
# Knowledge
Manage knowledge bases and upload documents over /knowledge/* with an API key scoped knowledge:read / knowledge:write.
Source: https://docs.polimorf.app/docs/api/knowledge
Manage knowledge bases and their documents with an API key. Every route is
scoped to the **key's own workspace** — you never pass an organization or
workspace id. Reads require the `knowledge:read` scope; writes require
`knowledge:write` (see [Authentication](/docs/authentication)). For a guided
walkthrough and SDK snippets, see [Knowledge bases](/docs/knowledge).
## Sources
### `POST /knowledge/sources`
Create a knowledge base. Requires `knowledge:write`.
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------- |
| `name` | string | yes | Display name of the base. |
| `description` | string | no | Optional description. |
```bash
curl https://api.polimorf.app/knowledge/sources \
-H "Authorization: Bearer $POLIMORF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Product FAQ" }'
```
### `GET /knowledge/sources`
List the bases in the workspace (`knowledge:read`).
### `DELETE /knowledge/sources/:sourceId`
Delete a base **and all of its documents** (`knowledge:write`).
## Documents
### `GET /knowledge/sources/:sourceId/documents`
List a base's documents with their ingestion `status` (`PENDING`, `PROCESSING`,
`READY`, `FAILED`). Requires `knowledge:read`.
### Direct upload — `POST /knowledge/sources/:sourceId/documents/upload`
The one-shot path: send the document bytes **base64-encoded inline** and the
server stores and ingests it. Best for small documents; large binaries should
use the presigned flow below. Requires `knowledge:write`.
| Field | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------------------------------------- |
| `filename` | string | yes | Filename with a supported extension (see [types](/docs/knowledge)). |
| `contentBase64` | string | yes | The document bytes, base64-encoded. |
| `contentType` | string | no | Derived from the filename extension when omitted. |
```bash
curl https://api.polimorf.app/knowledge/sources/$SOURCE_ID/documents/upload \
-H "Authorization: Bearer $POLIMORF_API_KEY" \
-H "Content-Type: application/json" \
-d "{ \"filename\": \"faq.md\", \"contentBase64\": \"$(base64 < faq.md)\" }"
```
Returns the created document (`status: "PENDING"`); ingestion runs in the
background.
### Presigned upload (large files)
Use this three-step flow to stream large binaries straight to object storage
without routing the bytes through the API. The SDK's
`client.knowledge.uploadDocument` performs all three steps for you.
1. **Request a presigned URL** — `POST /knowledge/sources/:sourceId/documents`
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------- |
| `filename` | string | yes | Filename with a supported extension. |
| `contentType` | string | yes | Must match the filename's extension. |
| `byteSize` | number | yes | Size of the file in bytes (≤ 25 MiB). |
Returns `{ document, upload: { url, method: "PUT", headers, expiresAt } }`.
2. **Upload the bytes** — `PUT` the file to `upload.url` with the returned
`upload.headers`. This request goes to object storage, not the API, and
carries no API key.
3. **Complete** — `POST /knowledge/sources/:sourceId/documents/:documentId/complete`
confirms the upload and enqueues ingestion. Returns the document.
### `POST /knowledge/sources/:sourceId/documents/:documentId/retry`
Re-run ingestion for a `FAILED` document (`knowledge:write`).
### `DELETE /knowledge/sources/:sourceId/documents/:documentId`
Delete a single document and its stored bytes (`knowledge:write`).
## Errors
| Status | Code | Meaning |
| ------ | ------------------ | ------------------------------------------------------------- |
| `400` | `VALIDATION_ERROR` | Unsupported extension, type/extension mismatch, or too large. |
| `403` | `FORBIDDEN` | The key lacks `knowledge:read`/`knowledge:write`. |
| `404` | `NOT_FOUND` | No such source or document in the key's workspace. |
See [Errors](/docs/errors) for the full catalog.