Persist authenticated sessions with Profiles
Maintain authenticated sessions across Steel browser instances using profiles.
Scaffolds a starter project locally. Requires the Steel CLI.
A Steel profile is a named, long-lived browser identity. It holds everything a real Chrome user profile accumulates over time: cookies, localStorage, IndexedDB, history, installed extensions, autofill, site permissions. Every session you attach to the profile starts where the last one left off, and writes the user data directory back on release.
Two options on sessions.create wire it up. On the first run, mint a fresh profile:
session = await client.sessions.create({persistProfile: true,profileId: undefined,});const profileId = session.profileId;
persistProfile: true tells Steel to snapshot the browser data directory when the session ends. profileId: undefined means "create a new one." After the session is created, session.profileId holds the identifier. Store it. Every later run passes it back:
session = await client.sessions.create({persistProfile: true,profileId,});
The new browser opens as that identity. client.profiles.list(), client.profiles.retrieve(id), and client.profiles.delete(id) round out the surface.
How the demo works
index.ts uses demowebshop.tricentis.com, a public shopping cart demo that stores cart state in cookies. The flow lives in main() and three helpers in utils.ts:
- 1
selectOrCreateProfilecallsclient.profiles.list(), theninquirerprompts you to pick an existing profile or create a new one. Returnsundefinedto signal "mint a fresh one." - 2On a fresh run, Session #1 launches with
persistProfile: trueand noprofileId.addItemsToCartvisits three category pages (books, digital downloads, notebooks) and clicks the first add-to-cart button on each. Session #1 releases; Steel writes the profile snapshot. - 3Session #2 launches with the same
profileId.checkItemsInCartopens/cartand reads the rows. Same identity, different browser, same cart.
Select the saved profile on a later run and the menu skips step 2 entirely. One session spins up, finds the cart intact, and exits. That is the test: state survives across distinct sessions because the profile carries it.
Run it
cd examples/profiles-tscp .env.example .env # set STEEL_API_KEYnpm installnpm start
Get a key at app.steel.dev/settings/api-keys. Session viewer URLs print as the script runs. Open them in other tabs to watch each browser.
Your first-run output varies. Structure looks like this:
Steel Profiles Demo============================================================? Select a profile to use: Create a new profileSteel Session #1 created!View session at https://app.steel.dev/sessions/ab12cd34...Profile ID: prof_9f3c...Successfully logged inAdded item from Book categoryAdded item from Digital Download categoryAdded item from Notebook category3 items in cartItems added:1. Computing and Internet2. Music 23. FictionSession #1 releasedSteel Session #2 created!View session at https://app.steel.dev/sessions/ef56gh78...Found 3 items in cartFound your shopping cart!Session released
Full round-trip takes ~60 seconds. A second run against the saved profile takes ~30 seconds because step 2 is skipped.
Sessions go through client.sessions.release() in the finally block. Skipping it keeps browsers running until the 5-minute default timeout and delays the profile snapshot.
What persists
Profiles capture the full Chromium user data directory, not just cookies:
- Cookies and localStorage for every origin you visited.
- Login sessions you kept alive (bank, SaaS dashboard, email).
- IndexedDB entries for apps that cache state client-side.
- Installed extensions and their configuration.
- Autofill, history, bookmarks, site permissions.
Treat the profile like an account. Anyone who can call sessions.create({ profileId }) on your workspace can drive a browser logged in as you. Rotate or delete with client.profiles.delete(id) when the identity is done.
Make it yours
- Swap the target site. Replace the URLs in
login,addItemsToCart, andcheckItemsInCart. The profile plumbing does not change. - Seed a profile interactively. Create a session with
persistProfile: true, open the live viewer, sign in by hand, close the session. The profile keeps the login. Every scripted run after that reuses it. - One profile per identity. If you automate three accounts on the same site, create three profiles. Sharing a profile across accounts means one session's writes overwrite another's state on release.
- Read without writing back. Pass
persistProfile: falsewith an existingprofileIdto load the profile without snapshotting changes on release. Useful for risky runs that might corrupt state.
Related
Three recipes handle "start the browser already signed in." Pick by lifetime:
- credentials: Steel stores a username and password per origin and fills the login form each session. No browser state persists. Good when the form is standard and you want a stable, long-lived setup.
- auth-context: one-shot JSON snapshot of cookies and localStorage you capture from one session and replay into the next. Good when you log in once (SSO, MFA, magic link) and want to move the resulting state forward.
- Profiles (this recipe): long-lived named identity that accumulates everything (history, extensions, preferences, logins) across runs. Good when the browser itself is the unit of persistence.
Scaffolds a starter project locally. Requires the Steel CLI.
A Steel profile is a named, long-lived browser identity. It carries everything a real Chrome user data directory accumulates: cookies, localStorage, IndexedDB, history, installed extensions, autofill, site permissions. Attach a session to a profile and the browser opens where the last one left off. Release the session and Steel snapshots the data directory back into the profile.
Two arguments on client.sessions.create wire this up. The first run mints a profile by asking for persistence and passing no id:
session = client.sessions.create(persist_profile=True)profile_id = session.profile_id
persist_profile=True tells Steel to write the browser data directory back when the session ends. With no profile_id, Steel creates a fresh one and returns it on the session object. Store that id. Every later run passes it back:
session = client.sessions.create(persist_profile=True, profile_id=profile_id)
The new browser opens as that same identity. client.profiles.list(), client.profiles.retrieve(id), and client.profiles.delete(id) round out the surface.
How the demo works
main.py runs a straight two-session flow against demowebshop.tricentis.com, a public shopping cart demo that keeps cart state in cookies:
- 1Session #1 launches with
persist_profile=Trueand no profile id.add_first_book_to_cartopens/books, clicks the first add-to-cart button, and waits for.cart-qtyto move off(0). The session releases and Steel snapshots the profile. - 2Session #2 launches with the captured
profile_id.count_cart_rowsopens/cartand counts.cart tbody tr. A row count above zero means the cart survived a browser that no longer exists, carried forward by the profile.
This is a port of ../profiles-ts, reworked to run end to end without input. The TypeScript version opens an inquirer picker to choose an existing profile or mint a new one. This Python version drops the picker and always creates a fresh profile, then reuses it once, so the persistence round-trip happens in a single run.
Run it
cd examples/profiles-pycp .env.example .env # set STEEL_API_KEYuv run main.py
Grab a key at app.steel.dev/settings/api-keys. uv sync runs automatically on first uv run, so there is no separate install step. The two viewer URLs print as the script runs. Open them in other tabs to watch each browser.
Your output varies. Structure looks like this:
Steel Profiles Demo (Python)============================================================Session #1: https://app.steel.dev/sessions/ab12cd34...Profile ID: prof_9f3c...Added a book to the cart (cart shows Shopping cart (1))Session #1 released, snapshotting profile...Session #2: https://app.steel.dev/sessions/ef56gh78...Profile ID: prof_9f3c...Success: cart persisted across sessions with 1 item(s) via the profileReleasing session...Session releasedDone!
Both sessions go through client.sessions.release(). Session #1 is released inline so its profile snapshot lands before Session #2 opens; Session #2 is released in the finally block. Skipping release keeps browsers running until the default timeout and delays the snapshot.
What persists
Profiles capture the full Chromium user data directory, not just the cart cookie this demo touches:
- Cookies and localStorage for every origin you visited.
- Login sessions you kept alive (bank, SaaS dashboard, email).
- IndexedDB entries for apps that cache state client-side.
- Installed extensions and their configuration.
- Autofill, history, bookmarks, site permissions.
Treat a profile like an account. Anyone who can call client.sessions.create(profile_id=...) on your workspace can drive a browser logged in as you. Delete one with client.profiles.delete(id) when the identity is done.
Make it yours
- Swap the target site. Replace the URLs in
add_first_book_to_cartandcount_cart_rows. The profile plumbing does not change. - Seed a profile by hand. Create a session with
persist_profile=True, open the live viewer, sign in yourself, then release. The profile keeps the login, and every scripted run after that reuses it. - One profile per identity. Automating three accounts on the same site means three profiles. Sharing one across accounts lets a later session's snapshot overwrite an earlier one's state.
- Read without writing back. Pass
persist_profile=Falsewith an existingprofile_idto load a profile without snapshotting changes on release. Useful for risky runs that might corrupt state.
Related
Three recipes solve "start the browser already signed in." Pick by lifetime:
- credentials-py: Steel stores a username and password per origin and fills the login form each session. No browser state persists.
- auth-context-py: a one-shot JSON snapshot of cookies and localStorage captured from one session and replayed into the next.
- Profiles (this recipe): a long-lived named identity that accumulates everything across runs.
Other ports of this recipe: profiles-ts (interactive picker), profiles-go, profiles-rs. See the Playwright docs for the Python browser API.
Scaffolds a starter project locally. Requires the Steel CLI.
A Steel profile is a named, long-lived browser identity: the full Chromium user data directory (cookies, localStorage, IndexedDB, history, extensions, autofill, permissions) snapshotted on release and reloaded on the next attach. Two fields on SessionCreateParams drive it. persist_profile: Some(true) tells Steel to write the data directory back when the session ends. profile_id selects which identity to load: leave it None to mint a fresh one, or pass a captured id to resume.
The whole demo turns on one value moving between two create calls:
let session = client.sessions().create(SessionCreateParams {persist_profile: Some(true),..Default::default()}).await?;let profile_id = session.profile_id.clone().ok_or("no profile_id")?;
SessionCreateParams derives Default, so struct-update syntax sets only the two profile fields and leaves the rest at their server defaults. The first session returns a profile_id; the second passes it back with profile_id: Some(profile_id.clone()) and the same persist_profile: Some(true). Same identity, a brand new browser.
What the demo does
This recipe is non-interactive. The TypeScript sibling prompts you to pick a profile with inquirer; here both sessions run end to end with no input, so a single cargo run proves the round trip. It drives demowebshop.tricentis.com, a public shopping cart demo that keeps cart state in the browser, over CDP with chromiumoxide:
- 1Create session #1 with
persist_profile: Some(true), capturesession.profile_id, connect, open/books, and click the first add-to-cart button (.product-box-add-to-cart-button, falling back toinput[value="Add to cart"]). Waiting for.cart-qtyto appear confirms the click landed. - 2Release session #1 so Steel writes the profile snapshot, then sleep ~3 seconds to let the write settle.
- 3Create session #2 with the same
persist_profile: Some(true)and the capturedprofile_id, connect, open/cart, and count.cart tbody trrows with a one-linepage.evaluate. More than zero rows means the cart crossed the session boundary.
Each chromiumoxide connection spawns a handler task (tokio::spawn) to pump CDP events; handle.abort() stops it before the session is released.
Run it
cd examples/profiles-rscp .env.example .env # set STEEL_API_KEYcargo run
Get a key at app.steel.dev/settings/api-keys. Both session viewer URLs print as the run proceeds. Open them in other tabs to watch each browser.
Creating Steel session #1 with a fresh persisted profile...Profile ID: prof_9f3c...Session #1 live at https://app.steel.dev/sessions/ab12cd34...Added the first book to the cartSession #1 releasedCreating Steel session #2 from profile prof_9f3c...Session #2 live at https://app.steel.dev/sessions/ef56gh78...Found 1 item(s) in the cartSession #2 releasedProfile persistence confirmed: the cart survived across sessions
A full round trip takes ~30 seconds. Both sessions go through client.sessions().release(...) before the program exits; skip it and the browsers idle until the 5-minute default timeout, which also delays the profile snapshot.
Make it yours
- Swap the target. Change
BOOKS_URL,CART_URL, and the selectors. The two-createprofile plumbing stays the same for any site whose state lives in the browser. - Resume an existing profile. Skip session #1 and start at session #2 with a
profile_idyou saved earlier. Seed it once by hand: create a session withpersist_profile: Some(true), sign in through the live viewer, release, and reuse the id forever. - Read without writing back. Pass
persist_profile: Some(false)with an existingprofile_idto load the identity without snapshotting changes on release. Good for risky runs that might corrupt state. - Manage the identity.
client.profiles().list(),retrieve, anddeleteround out the surface. Treat a profile like an account: anyone who can callcreatewith its id drives a browser logged in as you.
Related
profiles-ts · profiles-py · profiles-go · auth-context-rs · chromiumoxide
Scaffolds a starter project locally. Requires the Steel CLI.
A Steel profile is a named, long-lived browser identity. It carries everything a real Chrome user profile accumulates: cookies, localStorage, IndexedDB, history, installed extensions, autofill, site permissions. Attach a session to a profile and the browser opens where the last one left off; on release, Steel writes the user data directory back to the profile.
Two fields on Sessions.Create wire it up, both through the steel.F(...) field wrapper. To mint a fresh profile, pass PersistProfile: steel.F(true) and leave ProfileID unset. The created *steel.Session exposes .ProfileID. Store it. Every later run passes it back as ProfileID: steel.F(profileID) alongside PersistProfile, and the browser opens as that identity.
Non-interactive by design
The TypeScript sibling opens an inquirer menu so you can pick an existing profile or create a new one. This Go port drops the picker and runs the full round-trip end to end in one invocation: seedCart mints a profile and adds an item, the program sleeps about three seconds so the snapshot settles, then verifyCart opens a second session from the same ProfileID and counts the cart rows. Nothing to click. To reuse a profile from a previous run, read the printed Profile ID and feed it into Sessions.Create yourself.
chromedp talks CDP directly: chromedp.Evaluate runs the cart logic in the page (document.querySelector(".product-box-add-to-cart-button") with an input[value='Add to cart'] fallback, then .cart-qty for the header count and .cart tbody tr for the row count). chromedp.NewRemoteAllocator with chromedp.NoModifyURL attaches to the Steel browser over the websocket URL, the same idiom as the chromedp recipe.
Run it
cd examples/profiles-gocp .env.example .env # set STEEL_API_KEYgo mod tidygo run .
Get a key at app.steel.dev/settings/api-keys. Both session viewer URLs print as the program runs; open them in other tabs to watch each browser.
Steel Profiles Demo============================================================Session #1 created with a fresh profile.View live at https://app.steel.dev/sessions/ab12cd34...Profile ID: prof_9f3c...Adding the first book to the cart...Added item. Header cart count now reads "(1)".Releasing session #1...Waiting for the profile snapshot to settle...Session #2 created from profile prof_9f3c...View live at https://app.steel.dev/sessions/ef56gh78...Opening the cart in the new browser...Releasing session #2...------------------------------------------------------------Profile ID: prof_9f3c...Session #1 viewer: https://app.steel.dev/sessions/ab12cd34...Session #2 viewer: https://app.steel.dev/sessions/ef56gh78...Found 1 item(s) in the cart. Profile persistence works.
Both sessions release through the release helper deferred right after each Sessions.Create. Skipping release keeps browsers running until the default timeout and delays the profile snapshot.
Make it yours
- Swap the target site. Replace
booksURL,cartURL, and the threeEvaluatesnippets. The profile plumbing does not change. - Add more items. Loop the click snippet over several category pages before releasing session #1, and the whole cart rides the profile forward.
- Seed a profile by hand. Create one session with
PersistProfile: steel.F(true), open its live viewer, sign in manually, release. The login lives in the profile, and every scripted run after that reuses it viaProfileID. - Read without writing back. Pass
PersistProfile: steel.F(false)with an existingProfileIDto load the profile without snapshotting changes on release. Useful for risky runs that might corrupt state.
Related
Three recipes handle "start the browser already signed in." Pick by lifetime:
- auth-context: one-shot JSON snapshot of cookies and localStorage you capture from one session and replay into the next. Good when you log in once (SSO, MFA, magic link) and want to move that state forward.
- Profiles (this recipe): long-lived named identity that accumulates everything (history, extensions, preferences, logins) across runs. Good when the browser itself is the unit of persistence.
- Sibling ports: profiles-ts, profiles-py, profiles-rs.
Related recipes
Reuse authenticated sessions across browsers
Maintain authenticated sessions across Steel browser instances by capturing and reusing cookies and local storage.
Automate logins with the Credentials API
Use the Steel Credentials API with Playwright to automate flows with stored credentials.
Scrape a page to Markdown, screenshot, and PDF
Use the Steel TypeScript SDK's direct API to scrape a page to clean Markdown for LLM context, plus screenshot and PDF, with no browser library.