Build your own credential collection UI instead of using the hosted page. Poll for login fields, then submit credentials via the API.
Use the Programmatic flow when:
- You need a custom credential collection UI that matches your app’s design
- You’re building headless/automated authentication
- You have credentials stored and want to authenticate without user interaction
How It Works
Create Managed Auth and Start Authentication
Poll and Submit
Poll until flow_step becomes AWAITING_INPUT, then submit credentials
Handle 2FA
If more fields appear (2FA code), submit again—same loop handles it
Getting started
1. Create Managed Auth
const auth = await kernel.profiles.auth.create({
domain: 'github.com',
profile_name: 'github-profile',
});
2. Start Authentication
const login = await kernel.profiles.auth.login(auth.id);
To save credentials for automatic re-authentication:
const login = await kernel.profiles.auth.login(auth.id, {
save_credential_as: 'my-saved-creds',
});
3. Poll and Submit Credentials
A single loop handles everything—initial login, 2FA, and completion:
let state = await kernel.profiles.auth.retrieve(auth.id);
while (state.flow_status === 'IN_PROGRESS') {
// Submit when fields are ready (login or 2FA)
if (state.flow_step === 'AWAITING_INPUT' && state.discovered_fields?.length) {
const fieldValues = getCredentialsForFields(state.discovered_fields);
await kernel.profiles.auth.submit(auth.id, { fields: fieldValues });
}
await new Promise(r => setTimeout(r, 2000));
state = await kernel.profiles.auth.retrieve(auth.id);
}
if (state.status === 'AUTHENTICATED') {
console.log('Authentication successful!');
}
The discovered_fields array tells you what the login form needs:
// Example discovered_fields for login
[{ name: 'username', type: 'text' }, { name: 'password', type: 'password' }]
// Example discovered_fields for 2FA
[{ name: 'otp', type: 'code' }]
Complete Example
import Kernel from '@onkernel/sdk';
const kernel = new Kernel();
// Create managed auth
const auth = await kernel.profiles.auth.create({
domain: 'github.com',
profile_name: 'github-profile',
});
const login = await kernel.profiles.auth.login(auth.id);
// Single polling loop handles login + 2FA
let state = await kernel.profiles.auth.retrieve(auth.id);
while (state.flow_status === 'IN_PROGRESS') {
if (state.flow_step === 'AWAITING_INPUT' && state.discovered_fields?.length) {
// Check what fields are needed
const fieldNames = state.discovered_fields.map(f => f.name);
if (fieldNames.includes('username')) {
// Initial login
await kernel.profiles.auth.submit(auth.id, {
fields: { username: 'my-username', password: 'my-password' }
});
} else {
// 2FA or additional fields
const code = await promptUserForCode();
await kernel.profiles.auth.submit(auth.id, {
fields: { [state.discovered_fields[0].name]: code }
});
}
}
await new Promise(r => setTimeout(r, 2000));
state = await kernel.profiles.auth.retrieve(auth.id);
}
if (state.status === 'AUTHENTICATED') {
console.log('Authentication successful!');
const browser = await kernel.browsers.create({
profile: { name: 'github-profile' },
stealth: true,
});
// Navigate to the site—you're already logged in
await page.goto('https://github.com');
}
The basic polling loop handles discovered_fields, but login pages can require other input types too.
When the login page has “Sign in with Google/GitHub/Microsoft” buttons, they appear in pending_sso_buttons:
if (state.pending_sso_buttons?.length) {
// Show the user available SSO options
for (const btn of state.pending_sso_buttons) {
console.log(`${btn.provider}: ${btn.label}`);
}
// Submit the selected SSO button
await kernel.profiles.auth.submit(auth.id, {
sso_button_selector: state.pending_sso_buttons[0].selector
});
}
Remember to set allowed_domains on the managed auth to include the OAuth provider’s domain (e.g., accounts.google.com).
MFA Selection
When the site offers multiple MFA methods, they appear in mfa_options:
if (state.mfa_options?.length) {
// Available types: sms, email, totp, push, call, security_key
for (const opt of state.mfa_options) {
console.log(`${opt.type}: ${opt.label}`);
}
// Submit the selected MFA method
await kernel.profiles.auth.submit(auth.id, {
mfa_option_id: 'sms'
});
}
After selecting an MFA method, the flow continues—poll for discovered_fields to submit the code, or handle external actions for push/security key.
External Actions (Push, Security Key)
When the site requires an action outside the browser (push notification, security key tap), the step becomes AWAITING_EXTERNAL_ACTION:
if (state.flow_step === 'AWAITING_EXTERNAL_ACTION') {
// Show the message to the user
console.log(state.external_action_message);
// e.g., "Check your phone for a push notification"
// Keep polling—the flow resumes automatically when the user completes the action
}
Step Reference
The flow_step field indicates what the flow is waiting for:
| Step | Description |
|---|
DISCOVERING | Finding the login page and analyzing it |
AWAITING_INPUT | Waiting for field values, SSO button click, or MFA selection |
SUBMITTING | Processing submitted values |
AWAITING_EXTERNAL_ACTION | Waiting for push approval, security key, etc. |
COMPLETED | Flow has finished |
Status Reference
The flow_status field indicates the current flow state:
| Status | Description |
|---|
IN_PROGRESS | Authentication is ongoing—keep polling |
SUCCESS | Login completed, profile saved |
FAILED | Login failed (check error_message) |
EXPIRED | Flow timed out (5 minutes) |
CANCELED | Flow was canceled |
The status field indicates the overall managed auth state:
| Status | Description |
|---|
AUTHENTICATED | Profile is logged in and ready to use |
NEEDS_AUTH | Profile needs authentication |