API reference
All Focused Sessions APIs are imported from appilot.
startFocusedSession(request)
Starts a session through the assistant surface embedded on the page (widget, or the Appilot browser extension). Returns a promise of a session handle.
interface FocusedSessionStartRequest {
/** Session Template to instantiate (recommended). */
template_id?: string;
/** Inline mission (development deployments only). */
mission?: FocusedSessionMission;
/** Values for the template's declared variable slots. */
variables?: Record<string, string>;
}
Exactly one of template_id / mission is required. Variable values are
plain strings; they are sanitized server-side before entering the mission.
The result
interface FocusedSessionStartResult {
focused_session_id: string;
conversation_id: number; // the conversation the session owns, 1:1
name: string; // resolved display name
presentation?: {
chrome?: 'ambient' | 'immersive'; // immersive hides ambient furniture
opening?: 'agent' | 'user'; // agent speaks first, or waits
input_placeholder?: string; // replaces "Ask anything about this page"
exit_label?: string; // relabels the exit affordance
};
}
presentation is resolved server-side from the template and applied by the
assistant surface. Your page does not have to do anything with it. It is a
closed set of copy and behaviour slots on purpose: no colour, size, layout or
markup crosses this boundary.
The handle
interface FocusedSessionPageHandle {
focusedSessionId: string;
conversationId: number;
name: string; // resolved display name
onOutcome(cb: (outcome: Record<string, unknown>) => void): () => void;
onEnded(cb: (event: { focusedSessionId: string; status: FocusedSessionEndStatus }) => void): () => void;
end(): Promise<void>;
}
type FocusedSessionEndStatus = 'abandoned' | 'expired' | 'failed' | 'ended';
onOutcome(cb)fires at most once per session, with the schema-validated outcome object. Returns an unsubscribe function.onEnded(cb)fires exactly once when the session reaches a terminal state without an outcome: abandoned by the server, expired, failed, or ended by your ownend()call. It is mutually exclusive withonOutcome, so subscribe to both. Without it, a page waiting for an outcome waits forever on a session that died.end()abandons the session. Abandoned sessions produce no outcome.
Resuming after a reload
A full page reload drops the handle, not the session. Three calls re-attach:
import {
resumeFocusedSession,
getResumableFocusedSessionId,
forgetFocusedSession,
} from 'appilot';
const handle = await resumeFocusedSession();
if (handle) {
handle.onOutcome(saveEvidence);
handle.onEnded(({ status }) => showEndedState(status));
} else {
showActivityFinishedState();
}
resumeFocusedSession(id?)resolves to a handle when the session is still running and belongs to the current identity, and tonullotherwise: nothing remembered, no assistant surface on the page, the session already ended, or it is somebody else's. Ownership is checked server-side, so holding an id proves nothing.getResumableFocusedSessionId()reads the id this tab remembers, without a round trip. Use it to decide whether to offer a "continue" affordance at all.forgetFocusedSession()drops the remembered id without ending the session server-side.
Inline missions (development)
interface FocusedSessionMission {
name: string;
prompt: string; // the mission text
allowed_tools?: string[]; // tool names; empty = none
kb_doc_ids?: string[]; // knowledge allowlist
outcome_schema: object; // JSON Schema for the outcome
max_turns?: number; // turn budget
}
Rejected unless the deployment enables runtime missions. Production integrations always use Session Templates.
Session semantics
- Identity: sessions require an identified user; anonymous visitors cannot start one.
- Conversation: each session owns its conversation. Turns sent while the session is active belong to it regardless of any other conversation state on the page.
- Scope: the assistant can only use the mission's allowed tools and knowledge scope. It redirects requests outside the mission back to it.
- Completion: the assistant produces the outcome only when the mission's completion criteria are met, and the outcome always satisfies the declared schema.
- Turn budget: when the template sets
max_turnsand it is exhausted, the session ends as abandoned and further turns are refused. - Presentation:
startFocusedSessionresolves the template's presentation and returns it on the result. The assistant surface applies it (immersive chrome, an agent-spoken opening, activity-specific input and exit copy); your page does not have to do anything with it. It is a closed set of copy and behavior slots by design, so it never restyles the panel. - Preflight: when the template declares mandatory grounding, it runs
before the session exists. If it fails,
startFocusedSessionREJECTS and no session is created, so your page never enters its running state and can render its own error. Handle the rejection; do not assume a resolved promise means the assistant is ready.
Verifying outcomes server-to-server
The outcome your page receives via onOutcome traveled through the user's
browser, which makes it fine to RENDER but not to TRUST: anything your
frontend later posts to your backend could have been altered by the user.
When an outcome feeds a decision (an assessment result, a record, anything
evidence-grade), your backend must re-read the authoritative copy from
Appilot before storing it:
GET {APPILOT_API_URL}/widget/focused-sessions/{focused_session_id}
X-Widget-Key: wk_live_... (your publishable key)
X-Widget-Secret: wsk_secret_... (your SERVER secret; never in page code)
Response:
{
"focused_session_id": "fs_...",
"status": "completed",
"mission_source": "template",
"mission_name": "Conversational assessment",
"template_semantic_id": "conversational-assessment",
"variables": { "learner_ref": "your-app:learner-123" },
"outcome": { "...": "the schema-validated result" },
"transcript": [
{ "role": "assistant", "text": "Let's begin. Tell me about...", "at": "..." },
{ "role": "user", "text": "Well, first I would...", "at": "..." }
],
"turns_used": 8,
"started_at": "...", "ended_at": "...",
"user_external_id": "your-app:user-123",
"user_display_name": "Alex",
"user_id": null
}
The recommended pattern: the page posts only IDs to your backend
(focused_session_id plus your own references), your backend calls this
endpoint, checks status === 'completed' and user_external_id against the
expected user, and stores the outcome from THIS response, never the page's
copy. This authenticates the outcome; whether its content is CORRECT is
still your review model's call (the result is AI output).
Two fields exist for that review:
variablesare the sanitized start variables the session ran with, so your backend can bind the outcome to the right record (the learner, the case, the order) without trusting anything the page posted.transcriptis the session's conversation as{ role, text, at }turns (roleis"user"or"assistant"; the assistant's hidden mission kickoff is omitted). The outcome is the assistant's account of the session; the transcript is what actually happened. When the outcome feeds an evidence-grade decision, store both: when a reviewer questions the result, the transcript is the record they read.
Error codes
Surfaced on the start/end calls (HTTP) and mapped to the SDK's rejections:
| Code | Meaning |
|---|---|
SESSION_START_INVALID | Neither or both of template_id / mission supplied |
SESSION_TEMPLATE_NOT_FOUND | Unknown or inactive template for this app |
SESSION_DOMAIN_UNKNOWN | The page's domain is not registered, so no app scope could be resolved |
SESSION_VARIABLE_MISSING | A required template variable was not supplied |
SESSION_MISSION_INVALID | Inline mission missing prompt or object outcome schema |
SESSION_RUNTIME_MISSIONS_DISABLED | Inline missions are off on this deployment |
SESSION_NOT_FOUND | Unknown session id (or wrong organization) |
SESSION_NOT_ACTIVE | Turn sent to a completed/abandoned session |
SESSION_TURNS_EXHAUSTED | The turn budget ran out; the session is now abandoned |
SESSION_PREFLIGHT_FAILED | The template's mandatory grounding could not be fetched, so NO session was created. Carries the failing tool, the host status, and your backend's own code / request_id. Branch on this to show your own "not available" state |
SESSION_PREFLIGHT_TIMEOUT | The same, but your backend did not answer in time. Deliberately distinct: a slow backend is an outage, a missing entity is content |
SESSION_FAILED | Turn sent to a session whose mission could not be fulfilled. Distinct from SESSION_NOT_ACTIVE; reaches onEnded as status failed |