Choose a call
Use one path per conversation. Every path above already turns the content into long-term memory, so sending the same conversation down two of them makes Synap extract it twice. Send each conversation once explains the cases.
What to send
- Both sides of the conversation. Record the user’s turn and your agent’s reply. Together they are the conversation Synap serves back as short-term context and later extracts memories from; a reply without its question, or a question without its answer, loses half of that.
- Two roles only:
userandassistant. There is nosystemrole. The Python SDK raisesValueErrorfor any other role before sending, and the server refuses it with HTTP 422 (InvalidInputError). Your system prompt is instructions you control, not something the person said, so leave it out. - Not tool calls or reasoning. Those have their own calls; see Traces and tool calls.
- The person, on every call. Send
user_idevery time. On a B2B instance also sendcustomer_idon every call; without it the server refuses the turn (HTTP 400,InvalidInputError, on the REST calls). On a B2C instance senduser_idonly: acustomer_idis refused, usually by the SDK before the request leaves your process. See B2C vs B2B.
Record live turns
Record each turn as soon as you have it: the user’s message when it arrives, and the assistant’s reply after your agent produces it. Use oneconversation_id for every turn of a conversation. record_message requires it to be a UUID and raises InvalidConversationIdError (a subclass of InvalidInputError) for anything else, before sending.
record_message returns message_id, conversation_id, session_id and recorded_at once the server has stored the turn.
Over the gRPC stream
If your agent keeps a stream open withinstance.listen for real-time anticipation, send the turns on that stream with instance.send_message instead. Events with event_type user_message or assistant_message are stored in the conversation the same way record_message stores them. Set role and event_type together: the defaults are "user" and "user_message", so the reply needs both changed.
record_message matter:
send_messageraisesListeningNotActiveErroriflisten()has not been called.- It returns once the event is on the stream. The server stores the turn moments later, not before the call returns.
- The SDK does not check that the id is a UUID; the server converts any other string into a UUID derived from it. Use a UUID anyway, because
conversation.context.fetchandget_context_for_promptonly accept UUIDs, and you need the same id to read the conversation back.
Record several turns at once
When you buffer turns, for example writing the user’s message and the reply together after each exchange, or flushing a queue from a worker, send them in one call withrecord_messages_batch. Each message takes the same fields as record_message.
failed and described in errors, so check them. The response also carries total, succeeded and results. Problems the SDK can see before sending, such as a conversation id that is not a UUID, still raise for the whole batch.
For older conversations you are loading in bulk, use Import existing history instead.
Push a finished transcript
When Synap had no part in the conversation while it ran, for example a voice call handled by your telephony stack or a support chat you process after it closes, push the whole thing at the end withingest_transcript. One call records every turn as a conversation, queues extraction and starts a summary of the conversation, then returns right away with an ingestion_id.
conversation_idcan be any string. Unlikerecord_message, the SDK does not require a UUID. The server converts a non-UUID string into a UUID derived from it, so the same string always maps to the same conversation. The response returns both:conversation_idis the UUID form, which is the one to pass toconversation.contextcalls, andexternal_conversation_idis the string you sent.transcriptis a list of turns or a string. Prefer the list: each turn keeps its owntimestamp, aspeakerdisplay label and its ownmetadata. A string is split on lines that start with a label (User:,Assistant:,Human:,AI:,System:orBot:);UserandHumanbecome user turns and every other label becomes an assistant turn. Typed turns acceptuserandassistantonly. A transcript can hold up to 2,000 turns.- Send the times.
ended_at,started_ator per-turn timestamps tell Synap when the conversation happened, which is what dates its memories and orders them against newer ones. When none is present, the response carries a warning saying so. Timestamps without a timezone are read as UTC. analysisis your own JSON about the call (up to 64 KB), such as a disposition or sentiment from your platform. Synap stores it as is, returns it in conversation-summary fetches, and gives it to extraction as hints. Memories still have to come from what was said in the transcript.conversation_typeis a free label up to 64 characters, such as"voice"or"chat".metadata(up to 64 KB) is stored on the conversation.
warnings list through resp.raw["warnings"]; in TypeScript it is resp.warnings.
Sending the same conversation_id again
A transcript is never replaced, so give every call its own
conversation_id. The SDKs still define TranscriptConflictError for a conflicting transcript, but the current server accepts the push as described above instead of refusing it.
Import a conversation from another system
To bring in one conversation that happened somewhere else, such as a chat export from a previous tool, send it as a document withmemories.create and document_type="ai-chat-conversation" (the default for memories.create). Put one turn per line, starting with User: or Assistant:, so Synap can split a long conversation at turn boundaries. Set document_created_at to when the conversation took place, so its memories are dated correctly.
ingest_transcript. For many conversations at once, use memories.batch_create as described in Import existing history.
What happens to recorded turns
Short-term context: right away
record_message and record_messages_batch return only after the server has stored the turn, so the next fetch of that conversation’s context includes it, from any process. The SDK also keeps a local copy of the turns it sent and adds them to the next context fetch in the same process. That covers send_message, whose turns reach the server moments after the call returns. This read-your-writes behavior is on by default in both SDKs.
Long-term memory: in the background
Synap does not extract memories from each turn the moment you record it. In the background, as the conversation grows and after it goes quiet or ends, Synap sends the recorded turns through the same extraction pipeline thatmemories.create uses. You do not call memories.create for turns you recorded. Context, end to end follows a message through this, and Context compaction covers the compacted context you can read back while the conversation runs.
A transcript pushed with ingest_transcript is extracted once, from the transcript itself. Synap does not extract that conversation’s turns a second time later. The ingestion_id it returns is the handle for memories.status and memories.wait_for_completion; record_message returns no ingestion_id, because nothing is extracted at that moment.
Send each conversation once
- Recorded turns plus
memories.createof the same text. The turns are already on their way to long-term memory. Amemories.createcall with the assembled conversation is a separate document, and the duplicate check only catches identical text, so Synap extracts the same content twice. Usememories.createfor content that is not a recorded turn. - Recorded turns plus
ingest_transcriptunder the same id. Synap accepts the transcript and extracts it again, leaving the recorded turns in place. Do this only when the transcript adds something the live turns lack, such asanalysis, start and end times, or a corrected version from your speech system.
Deeper scope ladders
On the default ladder,customer_id and user_id say everything needed. If your scope ladder has a level between customer and user, pass scope_path, naming every level down to the one the conversation belongs to:
record_messageandingest_transcripttakescope_path, and each message inrecord_messages_batchcan carry its ownscope_path.memories.createtakes the same map under the namescope.send_messagecannot carry a path. Userecord_messagefor a conversation that needs one.
Optional fields
Common mistakes
- A new
conversation_idon every turn. Each turn then becomes its own one-message conversation, and short-term context never holds more than that turn. Generate the id once per conversation and reuse it. - A non-UUID id on the live path.
record_messagerejects it outright. Onsend_messageit is accepted but stored under a derived UUID, which you cannot pass toconversation.context.fetch. customer_idon B2C, or none on B2B. Both are refused. Check which kind your instance is in B2C vs B2B.- Recording
systemmessages or tool output as turns. Onlyuserandassistantare accepted. Send tool activity with the calls in Traces and tool calls. - Sending a conversation twice. Recorded turns plus
memories.create, or a live conversation plus a transcript of it, extracts the content twice. - Reusing a call’s id for the next call. The second transcript is extracted, but its turns are not recorded as a conversation (
turns_recorded=0), and itsanalysis, type and times replace the first call’s on the conversation. Give each call its own id. - Recording more turns after pushing a transcript. Synap treats a transcript-pushed conversation as finished and does not extract later turns of it into long-term memory. Start a new conversation instead.
- Ignoring batch results.
record_messages_batchdoes not raise for a message that failed. Checkfailedanderrors. - Transcripts without times. Without
ended_at,started_ator turn timestamps, Synap cannot tell when the conversation happened.
Related pages
- Method references:
conversation.record_message,conversation.record_messages_batch,conversation.ingest_transcript,instance.send_message,memories.create - Context compaction: reading a long conversation back as compact, prompt-ready context
- Context, end to end: what happens to a message after you send it
- gRPC vs REST: choosing between
send_messageandrecord_message - Files: documents and uploads
- Import existing history: loading past conversations in bulk