Duet Machines
Traffic · Import files

Traffic

Import files

Upload a file of past production conversations, and one of their outcomes, to build the migration report with no integration. Every line is checked first; a file is stored whole or not at all.

#Conversations (JSONL)

On the agent's Traffic, Add conversations, Import a file, then Import JSONL, or POST /api/agents/<agent uid>/conversation-imports?filename=<name> with the file as the body. Each line is one whole, ended conversation in OpenAI Chat Completions format (pretty-printed here; in the file it is one line). Download the example file: three invented conversations.

{"external_id": "nw-1001", "started_at": "2026-10-20T14:02:11Z", "user_id": "cust_0042",
 "model": "gpt-5.4-mini", "metadata": {"channel": "chat"},
 "tools": [{"type": "function", "function": {"name": "get_booking", "parameters": {"type": "object",
   "properties": {"code": {"type": "string"}}, "required": ["code"]}}}],
 "messages": [
   {"role": "system", "content": "You are Ava, the support agent of Northwind Rentals."},
   {"role": "user", "content": "Hi, can you move booking NW-7731 to Friday?"},
   {"role": "assistant", "content": null, "tool_calls": [{"id": "call_1", "type": "function",
     "function": {"name": "get_booking", "arguments": "{\"code\": \"NW-7731\"}"}}],
     "usage": {"prompt_tokens": 412, "completion_tokens": 18,
       "prompt_tokens_details": {"cached_tokens": 0}}},
   {"role": "tool", "tool_call_id": "call_1", "content": "{\"status\": \"confirmed\"}"},
   {"role": "assistant", "content": "Done: NW-7731 now starts Friday.",
     "usage": {"prompt_tokens": 468, "completion_tokens": 21}}]}
external_id
Required: your conversation id, up to 200 characters, unique per agent. Outcomes name it. An id the agent already has, or that another line of the file repeats, refuses the file.
started_at
Required: when it started, ISO 8601 with a UTC offset.
messages
Required, in order: system or developer (the instructions), user, assistant (content, tool_calls or both) and tool (tool_call_id and content, the call's result). content is a string or a list of text parts; name is allowed. Every tool call must be answered by a later tool message, and every tool message must answer an earlier call. Each answered call becomes a tool turn, status executed, where its answer is.
tools
Optional: the agent's tools as Chat Completions definitions ({"type": "function", "function": {name, description, parameters}}). With the system messages they are the conversation's config snapshot, so the judge cites the rules and tools it ran with; then every called tool must be one of them. Tools without a system message are refused. Without a system message there is no snapshot, as for conversations sent without config.
model
Optional: the model the agent ran on, as your code calls it. It goes into the snapshot and prices usage.
usage
Optional, on an assistant message: the usage object of the Chat Completions response that wrote it (prompt_tokens, completion_tokens, optionally total_tokens, prompt_tokens_details.cached_tokens and the other token details). It needs model, and lands on the message's text turn, else on its first answered call; see Cost.
user_id, metadata
Optional: a pseudonymous user id, and string values only.

Any other field is refused. Imported conversations are stored ended, in the production environment (an API key's own environment when it imports), redacted like every conversation, judged like the ones you send, and marked with their import: the conversation's details name the file, and the list's imported filter (the import's uid) shows one import's conversations. The migration report reads the last 30 days of traffic by started_at, so import conversations that started within them.

#Errors

Nothing is stored until every line is valid. A refused file answers 422 with every error, line by line: {"detail": {"errors": [{"line": 4, "field": "messages.2.tool_calls.0.id", "reason": "no tool message answers this call"}], "total": 1}}. line is 1-based as editors number lines, field the path inside the line (null for the line as a whole: not JSON, blank, over the limit), and the first 200 errors are listed with total counting them all. Traffic shows the same table under the button. Fix the file and import all of it again.

#Outcomes (CSV)

Declare the outcomes first (Traffic, Outcomes), then From a CSV file, or POST /api/agents/<agent uid>/outcome-imports with the file as the body. The header names these six columns, each once, in any order; download the example file.

conversation,id,name,occurred_at,value,currency
nw-1001,evt-5001,booking_kept,2026-10-23T10:00:00Z,,
nw-1002,evt-5002,refunded,2026-10-22T12:30:00Z,350.00,USD
conversation
The conversation's external_id: one the agent has, imported or sent.
id
Your event id, unique per agent: importing it again replaces the event, so a file imported twice counts once. Repeated in one file, it is refused.
name
One of the agent's declared outcomes.
occurred_at
ISO 8601 with a UTC offset, not before the conversation started.
value, currency
A money outcome's amount and its declared currency; both empty for an outcome without value.

Each row is checked as a sent outcome is, and the file is stored whole or refused with each row's line and reason (the header is line 1).

#Limits

  • A conversations file: at most 10 MB and 5,000 conversations, each line at most 1 MB.
  • An outcomes file: at most 2 MB and 10,000 rows.
  • Larger files answer 413 (over the size) or a refusal at the first line past the count: split them and import each part.
  • Each import counts as one ingest request against the workspace's 1200 a minute.