Skip to navigation

Importing conversations

Move a transcript from another system into Runtype so an Agent’s next turn continues where the old system left off. Runtype stores the transcript on the conversation and replays it on the first turn after the import.

An imported conversation is an API conversation, so the conversation that the API returns has source: "app". It isn’t added to a Persona widget visitor’s history, and you can’t continue a widget conversation with a stored-history turn. For widget history, see Conversation history in an embedded widget.

Before you begin

You need the following:

  • A Runtype API key with the Records (Write) permission to create and update conversations, Records (Read) to read them back, and Agents (Execute) to run the Agent with POST /v1/agents/{id}/execute. To run it with POST /v1/dispatch instead, the key needs Flows (Execute). For more information, see Managing API keys.
  • A saved Agent that you own. Claude Managed Agents and external Agents keep their own history, so they can’t continue an imported conversation.

Import a conversation

To import a conversation and continue it, follow these steps:

  1. Create the conversation with POST /v1/conversations. Send source, agentId, and the first chunk of messages. The response contains the conversation id.
  2. Optional: Send each later chunk with PUT /v1/conversations/{id} and messagesMode: "append", in order. For more information, see Import a long transcript in chunks.
  3. Read the conversation back to check the import. For more information, see Check the import.
  4. Run the next turn with history: "stored". For more information, see Continue the conversation.

The following request imports a conversation in which the assistant called a tool, with the tool result in place, so that the next turn can replay the whole exchange:

import { RuntypeClient } from '@runtypelabs/sdk'
const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })
const conversation = await client.conversations.create({
title: 'Refund for order 1182',
agentId: 'YOUR_AGENT_ID',
ownerId: 'cust_4471',
source: { system: 'intercom', externalId: 'conv_88213' },
messages: [
{ role: 'user', content: 'Where is my refund for order 1182?' },
{
role: 'assistant',
content: '',
toolCalls: [
{ toolCallId: 'call_1', toolName: 'lookup_order', args: { orderId: '1182' } },
],
},
{
role: 'tool',
content: '',
toolResults: [
{ toolCallId: 'call_1', toolName: 'lookup_order', result: { status: 'refunded' } },
],
},
{ role: 'assistant', content: 'Your refund was issued on August 30.' },
],
})

Replace the following:

  • YOUR_API_KEY: your Runtype API key.
  • YOUR_AGENT_ID: the ID of the Agent that continues the conversation.

The Runtype MCP server’s create_conversation and update_conversation tools accept the same top-level fields in snake case (agent_id, owner_id, messages_mode).

Conversation fields

POST /v1/conversations and PUT /v1/conversations/{id} accept the following fields:

FieldTypeNotes
messagesarrayThe transcript. For the message shape, see Message fields.
agentIdstringThe Agent that the conversation belongs to. For more information, see Bind an Agent and record the source.
sourceobjectWhere the transcript came from. With externalId, also the idempotency key for re-running an import.
ownerIdstring1 to 255 characters. The end user that the conversation belongs to. Filter by it with GET /v1/conversations?ownerId=.
titlestringUp to 500 characters. On create, defaults to New Chat. Give each imported conversation a distinct title, such as one that includes the source conversation ID. When a title is already in use, Runtype adds a short suffix, and a create can fail with 500 after several conversations share one title.
metadataobjectOptional key/value pairs stored on the conversation.
modelIdstringOptional. Stored on the conversation’s metadata.
systemPromptstringOptional. Stored on the conversation’s metadata.
messagesModestringPUT only. replace (the default) or append.

We recommend that you set ownerId to the end user’s ID in the source system. You can then list one user’s imported conversations.

Message fields

Each entry in messages has the following fields:

FieldNotes
rolesystem, user, assistant, or tool.
contentA plain string, or an array of text, image, file, reasoning, and asset_ref parts.
toolCallsAssistant messages only. Each entry is { toolCallId, toolName, args }.
toolResultsTool messages only, and required on them. Each entry is { toolCallId, toolName, result }.
idOptional. Runtype creates one when you omit it. Use the source system’s message ID to make the import resumable.
createdAtOptional timestamp string, stored exactly as sent. Runtype creates an ISO 8601 value when you omit it.
authorOptional { type: "end_user" | "operator" | "agent" | "system", id?, externalId?, name? }.
metadataOptional key/value pairs stored with the message.
modelOptional. The model that wrote an assistant message. Runtype stores it as sent. On an imported message, it is informational only.

author records who wrote the message in the system that you’re migrating from, so a role: "user" turn stays attributable to a specific person. Runtype stores and returns it as you sent it. Runtype doesn’t send it to a model.

Runtype stores an imported reasoning part but leaves it out of the replay. A model’s thinking signature is bound to the provider that produced it, so a foreign one would fail the next turn. This is true even when the imported message names a model.

A reply that a stored-history turn appends is different. Runtype stores the Agent’s own transcript: one assistant message and one tool message for each model round, in order, with reasoning parts and model on each assistant message. When the next turn runs on that same model, Runtype replays those reasoning parts. When the next turn runs on another model, Runtype removes them. Runtype marks these messages itself, and drops that mark from any message you write, so only a reply the Agent wrote can replay its reasoning.

Transcript validation rules

Model providers reject a tool call without a matching result. Runtype checks the transcript when you write it, so a bad transcript fails at import and not at the next turn. A transcript must follow these rules:

  • User and system messages must have non-empty content.
  • Each message id must be unique in the transcript.
  • A tool message must carry at least one toolResults entry.
  • Every assistant toolCalls entry must be answered by a tool message before the next non-tool message.
  • Each toolCallId can be issued only once, and answered only once.
  • A result can’t answer a toolCallId that no earlier assistant message issued.
  • toolCalls is valid only on an assistant message, and toolResults only on a tool message.

If the source system doesn’t record tool calls, import the text turns only.

A transcript that breaks a rule returns 400 with a body similar to the following:

{
"error": "Validation Error",
"code": "CONVERSATION_TRANSCRIPT_INVALID",
"message": "messages[1]: assistant toolCalls entry has no matching tool result before the next non-tool message (toolCallId call_1)",
"details": {
"issues": [{ "index": 1, "toolCallId": "call_1", "reason": "tool_call_without_result" }]
}
}

Each entry in details.issues names the message index and one of the following reason values:

ReasonFix
duplicate_message_idGive each message a unique id.
duplicate_tool_call_idIssue each toolCallId only once.
duplicate_tool_result_idAnswer each toolCallId only once.
empty_contentGive the user or system message non-empty content.
tool_call_without_resultAdd a tool message that answers the call before the next non-tool message.
tool_calls_on_non_assistantMove toolCalls to an assistant message.
tool_message_without_resultsAdd toolResults to the tool message, or remove the message.
tool_result_without_callRemove the result, or add the assistant toolCalls entry that it answers.
tool_results_on_non_toolMove toolResults to a tool message.

Bind an Agent and record the source

agentId binds the conversation to one Agent. It must name an Agent that you own. Otherwise, the API returns 404. source records where the transcript came from, with the following fields:

FieldTypeNotes
systemstringRequired. 1 to 100 characters. For example, intercom.
externalIdstringOptional. 1 to 500 characters. The conversation’s ID in the source system.
importedAtstringOptional. An ISO 8601 date and time with a time zone offset, for example 2026-09-01T12:00:00Z.

Runtype stores both on the conversation’s metadata, as metadata.agentId and metadata.importSource.

A stored-history turn must run the bound Agent. A turn that names another Agent returns 409 with code CONVERSATION_AGENT_MISMATCH. If the conversation has no agentId, the first stored-history turn binds it to the Agent that runs it. To remove the binding, send agentId: null in a PUT request.

Re-run an import safely

source.externalId is an idempotency key. A fresh create returns 201 with "imported": true. If a conversation with the same source.system and source.externalId already exists, Runtype doesn’t create a second one. It returns 200 with the existing conversation and "imported": false:

{
"id": "YOUR_CONVERSATION_ID",
"imported": false,
"metadata": { "importSource": { "system": "intercom", "externalId": "conv_88213" } }
}

Runtype returns the existing conversation unchanged and doesn’t write the messages in the repeated request. Runtype still validates those messages first, so a repeated request with an invalid transcript returns 400. To add turns, use PUT with messagesMode: "append".

If two requests with the same source.system and source.externalId arrive at the same time, Runtype creates only one conversation. Use the source system’s conversation ID as externalId, and you can restart a migration script from the beginning without duplicates. Runtype doesn’t deduplicate a create without source.externalId.

Import a long transcript in chunks

PUT /v1/conversations/{id} takes messagesMode:

  • replace (the default) replaces the whole stored transcript with the messages that you send.
  • append adds your messages to the end of the stored transcript.

In append mode, Runtype skips a message whose id the conversation already stores. A chunk that you resend after a timeout adds nothing. Give every message the source system’s message ID so that you can retry each chunk on its own.

Runtype validates the stored transcript and the incoming chunk together. A tool message in the chunk can answer a toolCallId that the conversation already stores. A result that answers nothing still returns 400 CONVERSATION_TRANSCRIPT_INVALID. Keep an assistant toolCalls message and the tool message that answers it in the same chunk. A chunk that ends on an unanswered tool call is rejected.

The following request appends one message to the conversation:

await client.conversations.update('YOUR_CONVERSATION_ID', {
messagesMode: 'append',
messages: [{ id: 'src_412', role: 'user', content: 'Can you resend the receipt?' }],
})

Replace YOUR_CONVERSATION_ID with the id that the create request returned.

If another write changes the conversation during an append, the API returns 409 with code CONVERSATION_MODIFIED and writes nothing. Read the conversation again and resend the chunk.

Check the import

To check the import, read the conversation back with GET /v1/conversations/{id}:

curl https://api.runtype.com/v1/conversations/YOUR_CONVERSATION_ID \
-H 'Authorization: Bearer YOUR_API_KEY'

In the CLI, run runtype conversations get <conversation-id>.

Check the number of messages, the id of the last message, and metadata.importSource. If a chunked import stopped partway, the last message id tells you which chunk to send next.

Size limits

Limits apply to the whole conversation after the write. An append counts the stored transcript plus the incoming messages. The following table lists the limits:

LimitValue
Messages on one conversation10,000
Content of a single message1 MB
All message content on one conversation32 MB
The conversation’s metadata256 KB

A write that exceeds any of these returns 400 and changes nothing. Split a transcript that doesn’t fit across several conversations, or drop the oldest turns.

Every stored-history turn adds messages to the conversation. Leave room under the limits. A turn that would exceed them doesn’t run, and a reply that would exceed them isn’t stored.

Attachments

To import an attachment, send it inline as an image or file content part with base64 data. Runtype moves inline media larger than 256 KB to asset storage when the write lands. Runtype doesn’t fetch attachment URLs for you.

When you create a conversation, Runtype applies the 1 MB per-message limit to the inline content before it moves the media. The base64 data of a message’s attachments must fit in 1 MB, which is about 750 KB of source file. PUT moves the media before it checks the limit, so it doesn’t have this restriction.

For a larger file, upload it first with POST /v1/assets, which needs the Assets (Write) permission. Then reference it with an asset_ref part that has the following fields:

  • type: asset_ref.
  • assetId: the assetId that the upload returned.
  • refKind: image or file.
  • mimeType: the file’s content type.
  • sizeBytes: the file’s size in bytes.
  • orgKey: required, but Runtype replaces it with your organization’s key on write.
  • filename: optional.

What import doesn’t do

An import is a write, not a run. It doesn’t fire surface webhooks, start an Agent turn, or count toward execution usage.

The transcript doesn’t carry an Agent’s long-term memory. Long-term facts that an Agent needs from the start are seeded through the memory API (POST /v1/runtime/memory/save, read back with POST /v1/runtime/memory/recall), not through messages. Those routes are an Enterprise-plan feature.

Continue the conversation

The turn after an import calls POST /v1/agents/{id}/execute with history: "stored", the conversationId from the import, and only the new user message. Runtype replays the stored transcript ahead of the new message, so the imported tool pairs reach the provider:

curl -X POST https://api.runtype.com/v1/agents/YOUR_AGENT_ID/execute \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"history": "stored",
"conversationId": "YOUR_CONVERSATION_ID",
"messages": [{ "role": "user", "content": "Can you resend the receipt?" }]
}'

POST /v1/dispatch takes the same history field, with agent.agentId and conversationId:

{
"agent": { "agentId": "YOUR_AGENT_ID" },
"conversationId": "YOUR_CONVERSATION_ID",
"history": "stored",
"messages": [{ "role": "user", "content": "Can you resend the receipt?" }]
}

With the default history: "inline", your application keeps the transcript and sends the whole messages array on every turn.

A stored-history turn has the following requirements on both routes:

  • The request names a saved Agent that isn’t a Claude Managed or external Agent. Inline Agents and flow dispatches are refused.
  • The request includes conversationId. Without it, the API returns 400 with code STORED_HISTORY_REQUIRES_CONVERSATION.
  • messages contains only the new messages and ends with a user message. The stored transcript plus the new messages must also follow the transcript validation rules. Otherwise, the API returns 400 with code STORED_HISTORY_DELTA_INVALID.
  • The request doesn’t send Prefer: respond-async or a steer request (x-runtype-concurrency: steer). On POST /v1/dispatch, it also isn’t a resume or a client-tool continuation.

Other refusals return 400 with code STORED_HISTORY_UNSUPPORTED.

When Runtype accepts the request, it appends your new messages to the stored transcript before the model runs. A failed turn still keeps the question. When the turn completes, Runtype appends the Agent’s reply, including any tool calls and results. An appended reply message can carry metadata.executionId, the ID of the execution that wrote it.

Runtype doesn’t append a reply for a turn that fails, or for a turn that stops to wait for a client-side tool result or an approval. To keep that reply in the transcript, read it from the execution’s events and append it with messagesMode: "append".

A stored-history turn can also return the following errors. Nothing runs in these cases:

StatusCodeCause and fix
404CONVERSATION_NOT_FOUNDThe conversation doesn’t exist, isn’t yours, or is a Persona widget conversation.
409CONVERSATION_MODIFIEDAnother write changed the conversation. Read it again and retry.
413MESSAGES_TOO_LARGEThe conversation is at a size limit. The code is in details[].code. Start a new conversation.

Dispatch messages also accept the tool role with toolCalls and toolResults, in the same shape that the transcript uses. An application that keeps history: "inline" can still replay full tool turns on every request. The messages override on POST /v1/dispatch/resume and POST /v1/dispatch/continue accepts the same shape. Answer every toolCalls entry with the tool message that follows it.

Next steps

Continue with these guides: