# 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.