Tutorial 17 of 18

How to Use AI Sessions for Multi-Turn Context

Create an AI session, persist its ID, and reuse it across follow-up prompts to maintain server-managed conversation context.

14 min read

An AI session groups multiple sendAI turns under one ID so the platform can maintain context across follow-up questions. Pass sessionId on subsequent calls to continue the same conversation.

How Sessions Work

Turn 1                             Turn 2
──────                             ──────
sendAI({ message })                sendAI({
  (no sessionId)                     sessionId: <from turn 1>,
})                                   message: { text: follow-up }
     │                                    │
     ▼                                    ▼
new session ID created             same session continued
  • On the first call, omit sessionId or pass null. The SDK creates a session and returns its ID in the response metadata.
  • On follow-up calls, pass that ID as sessionId.
  • Sessions are tied to senderId. Use a stable user ID for each end user, support ticket, or conversation owner.

Extracting sessionId (First Turn)

const first = await notifier.sendAI({
  senderId: "user-123",
  message: { text: "Start a new support session." },
});

const sessionId =
  first?.metadata?.packet?.id ??
  first?.metadata?.id;

Store sessionId in your database keyed by user, ticket, or chat thread.

Continuing a Session

await notifier.sendAI({
  senderId: "user-123",
  sessionId,
  message: {
    text: "Can you elaborate on the second point?",
  },
});

Sessions vs. messages[] History

ApproachWhen to use
sessionIdServer-managed continuity and simpler client code
Full messages[] on each callExact client-controlled context window and stateless servers
fetchAIHistoryAudit or rebuild a UI from stored turns

Transport

Use transport: "http" for session continuation.

JavaScript / TypeScript

Continue a multi-turn AI conversation by passing sessionId from the first response into follow-up sendAI calls.

Complete Multi-Turn Example

import { DNotifier } from "@dnotifier-realtime/dnotifier";

const USER_ID = "user-123";

const notifier = new DNotifier({
  appId: process.env.DNOTIFIER_APP_ID,
  secret: process.env.DNOTIFIER_SECRET,
  transport: "http",
  userId: USER_ID,
  onConnected: () => {},
  onMessage: () => {},
  onDisconnected: () => {},
});

await notifier.connect();

// Turn 1 — new session
const first = await notifier.sendAI({
  senderId: USER_ID,
  message: {
    text: "I need help with billing for order #4821.",
  },
});

const sessionId =
  first?.metadata?.packet?.id ??
  first?.metadata?.id;

if (!sessionId) {
  throw new Error("The AI response did not include a session id");
}

console.log("Session:", sessionId);
console.log("Reply:", first?.data?.content ?? first);

// Turn 2 — same session
const second = await notifier.sendAI({
  senderId: USER_ID,
  sessionId,
  message: {
    text: "What documents do I need to submit?",
  },
});

console.log("Follow-up:", second?.data?.content ?? second);

Persist Session Per Ticket

// After the first turn, save the id to your database.
await db.tickets.update(ticketId, {
  dnotifierSessionId: sessionId,
});

// On a later HTTP request, load and reuse it.
const ticket = await db.tickets.get(ticketId);
const response = await notifier.sendAI({
  senderId: USER_ID,
  sessionId: ticket.dnotifierSessionId,
  message: { text: userMessage },
});

Chat-Style Messages in a Session

const first = await notifier.sendAI({
  senderId: USER_ID,
  message: {
    messages: [
      {
        role: "system",
        content: "You are a billing specialist.",
      },
      {
        role: "user",
        content: "I was charged twice.",
      },
    ],
  },
});

const sessionId =
  first?.metadata?.packet?.id ??
  first?.metadata?.id;

await notifier.sendAI({
  senderId: USER_ID,
  sessionId,
  message: {
    messages: [
      {
        role: "user",
        content: "Here is my receipt number: R-9912.",
      },
    ],
  },
});

TypeScript Helper

function sessionIdFromResponse(
  response: unknown,
): string | undefined {
  const value = response as {
    metadata?: {
      packet?: { id?: string };
      id?: string;
    };
  };
  return value?.metadata?.packet?.id ?? value?.metadata?.id;
}

Troubleshooting

IssueCheck
Follow-up lacks contextVerify sessionId is passed and not regenerated
New session each timeStore and reuse the ID from the first response
Wrong user contextsenderId must match across turns

Dart / Flutter

Continue a multi-turn AI conversation by passing sessionId from the first response into follow-up sendAI calls.

Complete Multi-Turn Example

import 'package:dnotifier/dnotifier.dart';

const userId = 'user-123';

Future<void> main() async {
  final notifier = DNotifier(
    appId: 'your-app-id',
    secret: 'your-app-secret',
    transport: 'http',
    userId: userId,
    onConnected: () {},
    onMessage: (_) {},
  );

  await notifier.connect();

  // Turn 1 — new session
  final first = await notifier.sendAI(
    senderId: userId,
    message: {
      'text': 'I need help with billing for order #4821.',
    },
  );

  final sessionId = sessionIdFromResponse(first);
  if (sessionId == null) {
    throw StateError('The AI response did not include a session id');
  }

  print('Session: $sessionId');

  // Turn 2 — same session
  final second = await notifier.sendAI(
    senderId: userId,
    sessionId: sessionId,
    message: {
      'text': 'What documents do I need to submit?',
    },
  );

  print('Follow-up: $second');
}

String? sessionIdFromResponse(dynamic response) {
  if (response is! Map) return null;
  final metadata = response['metadata'];
  if (metadata is Map) {
    final packet = metadata['packet'];
    if (packet is Map && packet['id'] != null) {
      return packet['id'].toString();
    }
    if (metadata['id'] != null) {
      return metadata['id'].toString();
    }
  }
  return null;
}

Persist Session Per Ticket

class TicketStore {
  String? sessionIdForTicket(String ticketId) => _sessions[ticketId];

  void saveSession(String ticketId, String sessionId) {
    _sessions[ticketId] = sessionId;
  }

  final Map<String, String> _sessions = {};
}

Future<void> handleUserMessage(
  DNotifier notifier,
  TicketStore store,
  String ticketId,
  String text,
) async {
  var sessionId = store.sessionIdForTicket(ticketId);

  final response = await notifier.sendAI(
    senderId: userId,
    sessionId: sessionId,
    message: {'text': text},
  );

  sessionId ??= sessionIdFromResponse(response);
  if (sessionId != null) {
    store.saveSession(ticketId, sessionId);
  }
}

Chat-Style in a Session

final first = await notifier.sendAI(
  senderId: userId,
  message: {
    'messages': [
      {
        'role': 'system',
        'content': 'You are a billing specialist.',
      },
      {
        'role': 'user',
        'content': 'I was charged twice.',
      },
    ],
  },
);

final sessionId = sessionIdFromResponse(first);

await notifier.sendAI(
  senderId: userId,
  sessionId: sessionId,
  message: {
    'messages': [
      {
        'role': 'user',
        'content': 'Here is my receipt number: R-9912.',
      },
    ],
  },
);

Python

Continue a multi-turn AI conversation by passing session_id from the first response into follow-up send_ai calls.

Complete Multi-Turn Example

import asyncio
from dnotifier import DNotifier

USER_ID = "user-123"

def session_id_from_response(response) -> str | None:
    if not isinstance(response, dict):
        return None
    metadata = response.get("metadata")
    if isinstance(metadata, dict):
        packet = metadata.get("packet")
        if isinstance(packet, dict) and packet.get("id") is not None:
            return str(packet["id"])
        if metadata.get("id") is not None:
            return str(metadata["id"])
    return None

async def main():
    notifier = DNotifier(
        app_id="your-app-id",
        secret="your-app-secret",
        transport="http",
        user_id=USER_ID,
    )

    await notifier.connect()

    # Turn 1 — new session
    first = await notifier.send_ai(
        sender_id=USER_ID,
        message={
            "text": "I need help with billing for order #4821."
        },
    )

    session_id = session_id_from_response(first)
    if session_id is None:
        raise RuntimeError("The AI response did not include a session id")

    print(f"Session: {session_id}")

    # Turn 2 — same session
    second = await notifier.send_ai(
        sender_id=USER_ID,
        session_id=session_id,
        message={
            "text": "What documents do I need to submit?"
        },
    )

    print(f"Follow-up: {second}")

asyncio.run(main())

Persist Session Per Ticket

class TicketStore:
    def __init__(self):
        self._sessions: dict[str, str] = {}

    def session_id_for_ticket(self, ticket_id: str) -> str | None:
        return self._sessions.get(ticket_id)

    def save_session(self, ticket_id: str, session_id: str):
        self._sessions[ticket_id] = session_id

async def handle_user_message(
    notifier,
    store: TicketStore,
    ticket_id: str,
    text: str,
):
    session_id = store.session_id_for_ticket(ticket_id)

    response = await notifier.send_ai(
        sender_id=USER_ID,
        session_id=session_id,
        message={"text": text},
    )

    if session_id is None:
        new_session = session_id_from_response(response)
        if new_session:
            store.save_session(ticket_id, new_session)

Chat-Style in a Session

first = await notifier.send_ai(
    sender_id=USER_ID,
    message={
        "messages": [
            {
                "role": "system",
                "content": "You are a billing specialist.",
            },
            {
                "role": "user",
                "content": "I was charged twice.",
            },
        ],
    },
)

session_id = session_id_from_response(first)

await notifier.send_ai(
    sender_id=USER_ID,
    session_id=session_id,
    message={
        "messages": [
            {
                "role": "user",
                "content": "Here is my receipt number: R-9912.",
            },
        ],
    },
)