Sessions
Staying signed in across jobs: what a session stores, how jobs bind and lock it, how Kaabist checks it, what happens when it expires, and refresh programs.
A session is a stored, encrypted browser storage state that lets jobs start already signed in to a site. You create it once, from a first run or by uploading a state, and every job you bind to it skips the login, until the site expires it and a refresh program or a person signs in again.
#When to use it
Use a session for any program that works behind a login and runs more than once: a scheduled export, a report read every hour, a batch of lookups in a portal. Without one, every job signs in, which is slower, costs browser time, and on many sites triggers a one-time code or a "new device" warning each time. For a single run, a program that signs in without saving a session is enough.
#What a session is
{
"id": "ses_01J…",
"object": "session",
"name": "portal-login",
"origins": ["https://portal.example.com"],
"status": "valid",
"concurrency": "exclusive",
"refresh": { "program": "portal-report", "has_inputs": true },
"last_verified_at": "2026-09-20T09:00:00Z",
"last_used_at": "2026-09-20T11:42:00Z",
"locked_by": null,
"created_at": "…", "updated_at": "…"
}
status is valid, expired or unknown. origins scopes which sites the session's cookies may be sent to. The storage state itself is write-only: the object has no field for it. You can replace it and delete it; nothing (not the API, not the panel, not a support request) reads it back. It is encrypted with your organisation's own data key and exists in the clear only inside the worker running your job, for the length of that job.
#Creating a session
There are two ways.
From a job. A checkpoint step with save_session writes the browser's storage state to the named session, creating it if it does not exist and merging into it if it does:
{ "checkpoint": { "name": "logged_in", "save_session": "portal-login" } }
This is the usual way: the program signs in once, and the state it ends up with is what later jobs start from.
From a state you already have. POST /v1/sessions with a Playwright storageState:
{
"name": "portal-login",
"storage_state": { "cookies": [], "origins": [] },
"concurrency": "exclusive",
"refresh": { "program": "portal-report", "inputs": { "email": "[email protected]" } }
}
origins is derived from the state when you do not send it. PATCH /v1/sessions/{id} changes name, concurrency or refresh, or replaces storage_state.
#Binding a session to jobs
A job binds a session by name, with session in the job body or options.session in the program:
{ "options": { "session": "portal-login", "resume_from_session": true } }
The session is loaded before the first step. With resume_from_session: true, a job whose session is valid skips straight to the step after the checkpoint that saved it: the login steps run only when they are needed. A schedule binds a session the same way, through the program or the schedule's options, so every fire starts signed in.
#Two halves of being signed in
Being signed in is cookies and localStorage, and the tiers see them differently:
- the browser tiers load and save both, so a session saved by a browser job is complete;
- the http tier carries the cookies only and never writes a session back. A program that saves a session needs a browser, and its
checkpoint … save_sessiongives it one.
A session saved by a browser job therefore works at the http tier afterwards, but not the other way round.
#Locking
Two jobs signed in as the same person at the same time is how sites decide you are a robot, and on a site that allows one session at a time each job logs the other out. So a job holding a session holds a lease on it:
exclusive(the default): one job at a time. Another job bound to the session waits in the queue withmeta.blocked_reason: "session_locked", and a job that still cannot take the lease after a minute failssession_locked_timeout.shared: opt in for sites you know tolerate parallel sign-ins.
A job releases its lease when it ends, whichever way it ends. locked_by names the job holding it, and DELETE /v1/sessions/{id} answers 409 session_locked (with details.job_id) while one does.
#Checking that a session works
Sites expire sessions on their own schedule and never say so. Kaabist finds out in two places:
- Before the job. If nobody has checked the session in the last hour, a probe loads the root of the session's first origin with it, at the http tier, and reads the answer: a recognised sign-in page means
expired; a page that cannot be judged meansunknown, and the job runs and finds out. A session already known to be expired is not probed. A site whose signed-in page only appears after its JavaScript runs should setoptions.session_probe: "browser"on the program, which probes with a browser page load instead. - During the job. A step that lands on a sign-in page (the program's
login_pagelandmark, or a recognised login form) is classified as an expired session rather than a broken program.
POST /v1/sessions/{id}/verify runs the probe now and answers { "status", "checked_at" }. It costs 1 credit, or 5 where a browser probes.
#Expiry
When a session is found expired, its status becomes expired, a session.expired event is sent once, and the job does what the program's policies.on_session_expired says:
on_session_expired | What the job does |
|---|---|
refresh (default) | runs the session's refresh program and carries on; with no refresh program, or one that cannot finish, parks as session_expired |
park | parks as needs_attention with reason session_expired, for a person to sign in |
fail | fails with session_expired |
A parked session_expired job is resolved like any other park: a person takes over and signs in, or you share a hosted resolve page with whoever holds the account. See Human in the loop.
#Refresh programs
A refresh program is an ordinary program that knows how to sign in and ends with a checkpoint that saves the session. Name it in the session's refresh.
When a session expires mid-job, Kaabist runs the refresh program inside that job, in the same browser, so the page your program was on is the page that ends up signed in, and then retries the step that failed. Its steps appear in the job's timeline under session_refresh/. If the site asks for a one-time code, the refresh stops and your job parks for it as awaiting_input, exactly as an await_input step in your own program would; answering it resumes the refresh, not the whole program.
POST /v1/sessions/{id}/refresh runs the refresh program on demand as an ordinary job and answers 202 with it.
#Credentials
Storing a password is optional, discouraged, and off unless you turn it on. Kaabist keeps a refresh program's input only when all three are true:
- your organisation has enabled secret storage in its settings;
- the refresh program declares the input as
{ "type": "secret", "store": true }; - you supplied it in the session's
refresh.inputs.
Setting a session's refresh inputs is recorded in your organisation's audit log.
Offering a secret that cannot be stored is refused with 422 secret_storage_disabled rather than dropped, so you are never told a password was saved when it was not. A session with no stored credentials cannot refresh unattended: it parks and a person signs in, which is the mode we recommend.
#What is kept, and where
| Storage state | encrypted with your organisation's data key; write-only |
| Stored refresh inputs | the same, and only inputs marked store: true |
| Logs, snapshots, screenshots, webhooks | cookie names at most, never values |
| Deleting a session | removes the row and its state; an organisation data deletion removes every session |
#Worked example
The site at https://portal.example.com signs in with an e-mail and password, asks for a six-digit code, and sets a sid cookie. A program for it fills the form, parks on await_input for the code, then saves the session:
{ "id": "save", "checkpoint": { "name": "logged_in", "save_session": "portal-login" } }
It binds options.session: "portal-login" with resume_from_session: true, and sets policies.on_session_expired: "refresh".
First run. No session exists yet, so the job runs every step: it signs in, parks for the code, takes your answer, and at the save checkpoint creates portal-login. The envelope's meta.session is { "id": "ses_…", "name": "portal-login", "saved": true }.
Uploading instead. If you already hold a signed-in state, create the session directly and run the program against it:
// Storage state is write-only: Kaabist encrypts it and never returns it.
const session = await kaab.sessions.create({
name,
storage_state: {
cookies: [
{
name: "sid",
value: "example-session-cookie",
domain: new URL(site).hostname,
path: "/",
expires: -1,
httpOnly: true,
secure: false,
sameSite: "Lax",
},
],
origins: [],
},
origins: [new URL(site).origin],
concurrency: "exclusive", // one job at a time inside it
});
console.log(`${session.id}: ${session.name} is ${session.status}`);
Later runs. The session is valid, so the job jumps past the login to the report and never parks. Bind the same program to a schedule and every fire starts at the report:
const job = await kaab.jobs.create({
url: `${site}/api/echo`,
extract: { sid: { network: { url: "**/api/echo", json: "$.cookies.sid" } } },
session: name, // the job starts with the session's cookies and storage
options: { prefer_tier: "browser" }, // a browser, as a signed-in person would use
wait: 30,
});
console.log(`${job.id} ran in ${job.meta.session?.name}: sid=${JSON.stringify(job.data?.sid)}`);
Expiry. When the site expires the cookie, the next job's report page redirects to the sign-in form. The step is classified as an expired session, portal-login becomes expired, session.expired is sent, and the refresh program signs in again inside the job, parking for a new code if the site asks for one. With on_session_expired: "park" instead, the job parks as needs_attention: session_expired for a person.
#Pitfalls
- Two jobs, one account,
sharedconcurrency. On a site that allows one sign-in at a time, each job logs the other out. Keepexclusiveunless you know the site tolerates it. - A session for an http-only program. The http tier never saves a session; a program that only reads with cookies needs the session created by a browser job or uploaded.
- Storing passwords by default. Prefer
parkor a refresh program without stored secrets, and let a person sign in when the session expires. - Expecting to read the state back. You cannot. Keep your own copy if you need one, or create the session again from a job.
#Reference
- Create a session, update, verify, refresh
- The session object
checkpoint,set-session, program options, policies- Events (
session.expired) and error codes kaab sessions create