Think Chrome profiles. You have one for work, one for personal browsing, maybe one for testing. Each remembers its own logins, cookies, and settings. Steel Profiles work the same way — but you can spin them up programmatically for your agents.
Before today, every Steel session started fresh. Your agent would log in, complete a task, and when the session ended, everything was forgotten. Cookies cleared. Auth tokens gone. Next run? Log in again.
Now your agents can maintain persistent browser identities across sessions.
How It Works
A Profile stores browser state: cookies, extensions, credentials, localStorage, auth tokens, and fingerprints. When you start a session with a profileId, the browser loads that state. The agent picks up effectively where it left off — still logged in, still authenticated.
Here's the simplest case—an agent that needs to stay logged in across multiple runs:
// Day 1: Agent logs in and completes a taskconstsession = awaitclient.sessions.create({persistProfile:true});constbrowser = awaitchromium.connectOverCDP(`wss://connect.steel.dev?apiKey=${apiKey}&sessionId=${session.id}`);constpage = awaitbrowser.contexts()[0].pages()[0];awaitpage.goto('https://app.example.com/login');awaitpage.fill('#email','agent@company.com');awaitpage.fill('#password',process.env.PASSWORD);awaitpage.click('#login-button');// Session ends, state is savedawaitclient.sessions.release(session.id);console.log(`Profile ID: ${session.profileId}`);// Day 2: Resume where you left offconstresumedSession = awaitclient.sessions.create({profileId:session.profileId// Still logged in});
// Day 1: Agent logs in and completes a taskconstsession = awaitclient.sessions.create({persistProfile:true});constbrowser = awaitchromium.connectOverCDP(`wss://connect.steel.dev?apiKey=${apiKey}&sessionId=${session.id}`);constpage = awaitbrowser.contexts()[0].pages()[0];awaitpage.goto('https://app.example.com/login');awaitpage.fill('#email','agent@company.com');awaitpage.fill('#password',process.env.PASSWORD);awaitpage.click('#login-button');// Session ends, state is savedawaitclient.sessions.release(session.id);console.log(`Profile ID: ${session.profileId}`);// Day 2: Resume where you left offconstresumedSession = awaitclient.sessions.create({profileId:session.profileId// Still logged in});
// Day 1: Agent logs in and completes a taskconstsession = awaitclient.sessions.create({persistProfile:true});constbrowser = awaitchromium.connectOverCDP(`wss://connect.steel.dev?apiKey=${apiKey}&sessionId=${session.id}`);constpage = awaitbrowser.contexts()[0].pages()[0];awaitpage.goto('https://app.example.com/login');awaitpage.fill('#email','agent@company.com');awaitpage.fill('#password',process.env.PASSWORD);awaitpage.click('#login-button');// Session ends, state is savedawaitclient.sessions.release(session.id);console.log(`Profile ID: ${session.profileId}`);// Day 2: Resume where you left offconstresumedSession = awaitclient.sessions.create({profileId:session.profileId// Still logged in});
// Day 1: Agent logs in and completes a taskconstsession = awaitclient.sessions.create({persistProfile:true});constbrowser = awaitchromium.connectOverCDP(`wss://connect.steel.dev?apiKey=${apiKey}&sessionId=${session.id}`);constpage = awaitbrowser.contexts()[0].pages()[0];awaitpage.goto('https://app.example.com/login');awaitpage.fill('#email','agent@company.com');awaitpage.fill('#password',process.env.PASSWORD);awaitpage.click('#login-button');// Session ends, state is savedawaitclient.sessions.release(session.id);console.log(`Profile ID: ${session.profileId}`);// Day 2: Resume where you left offconstresumedSession = awaitclient.sessions.create({profileId:session.profileId// Still logged in});
Persist or Read-Only
When using an existing profile, you control whether changes write back:
// Read from profile, build on its historyconstsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:true// Appends new cookies, state changes});// Read from profile, keep it unchanged constsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:false// Default: no state mutation});
// Read from profile, build on its historyconstsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:true// Appends new cookies, state changes});// Read from profile, keep it unchanged constsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:false// Default: no state mutation});
// Read from profile, build on its historyconstsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:true// Appends new cookies, state changes});// Read from profile, keep it unchanged constsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:false// Default: no state mutation});
// Read from profile, build on its historyconstsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:true// Appends new cookies, state changes});// Read from profile, keep it unchanged constsession = awaitclient.sessions.create({profileId:'bc252fdd-0c22-43ae-9e53-c2ffaba1108a',persistProfile:false// Default: no state mutation});
Set persistProfile: true when you want to update the profile with new authentication state or app data. Keep it false for read-only operations or when running parallel sessions that shouldn't interfere with each other.
Human-in-the-Loop Setup
For complex auth flows, you can set up profiles manually. Create a session, open the debug URL, and handle the tricky parts yourself:
constsession = awaitclient.sessions.create({persistProfile:true});console.log(`Complete auth here: ${session.sessionViewerUrl}`);// Open the URL, handle 2FA, solve CAPTCHAs, accept terms, etc.// Wait for manual completion...awaitnewPromise(resolve=>setTimeout(resolve,60000));// Release the session - your auth state is savedawaitclient.sessions.release(session.id);// Now your agent uses your authenticated stateconstagentSession = awaitclient.sessions.create({profileId:session.profileId});
constsession = awaitclient.sessions.create({persistProfile:true});console.log(`Complete auth here: ${session.sessionViewerUrl}`);// Open the URL, handle 2FA, solve CAPTCHAs, accept terms, etc.// Wait for manual completion...awaitnewPromise(resolve=>setTimeout(resolve,60000));// Release the session - your auth state is savedawaitclient.sessions.release(session.id);// Now your agent uses your authenticated stateconstagentSession = awaitclient.sessions.create({profileId:session.profileId});
constsession = awaitclient.sessions.create({persistProfile:true});console.log(`Complete auth here: ${session.sessionViewerUrl}`);// Open the URL, handle 2FA, solve CAPTCHAs, accept terms, etc.// Wait for manual completion...awaitnewPromise(resolve=>setTimeout(resolve,60000));// Release the session - your auth state is savedawaitclient.sessions.release(session.id);// Now your agent uses your authenticated stateconstagentSession = awaitclient.sessions.create({profileId:session.profileId});
constsession = awaitclient.sessions.create({persistProfile:true});console.log(`Complete auth here: ${session.sessionViewerUrl}`);// Open the URL, handle 2FA, solve CAPTCHAs, accept terms, etc.// Wait for manual completion...awaitnewPromise(resolve=>setTimeout(resolve,60000));// Release the session - your auth state is savedawaitclient.sessions.release(session.id);// Now your agent uses your authenticated stateconstagentSession = awaitclient.sessions.create({profileId:session.profileId});
Handle 2FA, CAPTCHAs, or SSO flows once, then let your agent reuse that authenticated state for all future runs.
Why This Matters
No repeated authentication: Your agent logs in once. Every subsequent session picks up with valid auth tokens and cookies already in place.
Better stealth: Profiles build browsing history and reputation. Sites see a returning user, not a fresh bot on every request. Fewer CAPTCHAs, less anti-bot friction.
Multi-tenant workflows: Each customer gets their own profile. One authentication per tenant, reused across all their agent operations.
Parallel testing: Fork a baseline profile to test different agent strategies without sessions interfering with each other's state.
Simpler infrastructure: No more managing cookie jars in Redis or session blobs in S3. Steel handles state persistence, encryption, and retrieval.
Available Now
The Profiles API is live today in Steel Cloud, plus Python(v0.11.0+) and Node SDKs (v0.12.0+).