Install
$ agentstack add skill-descope-skills-descope-byos-builder ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Descope BYOS Builder
Translate Descope flow JSON exports into working React BYOS screens that call state.next(interactionId, form). The failures below are recorded from real BYOS sessions — every one cost 15–60 minutes the first time.
When to Use
- Building custom UI over a Descope flow while keeping Descope's flow engine (no client-side JWT parsing, full flow logic intact)
- Modifying an existing BYOS implementation after the underlying flow changed in the Descope console
- Debugging "flow completes but session stays wrong" / "button does nothing" / "session is anonymous" / "passkey ceremony aborts" symptoms
- Auditing whether an existing BYOS matches hosted-screen parity
- Adding post-auth promotion subflows (e.g.
add-passkeys) that run afterlogged-in
Don't use for: flows that fully work with the hosted `` widget (BYOS is a tradeoff — you give up flow edits propagating without code changes).
The Iron Rule
Ground every BYOS component in the exported flow JSON. Do not guess interaction IDs, output key names, or screen names. Every failure in the catalog starts with someone making up a value that looked reasonable.
Inputs Required (Ask the User First)
This skill cannot fetch flows from the Descope console — the agent has no console access. Before doing any work, ask the user to provide:
- Flow JSON files — exported from Descope console for the main flow AND every subflow it invokes (LoadSubflow
arguments.flowId), including post-auth promotion subflows (e.g.add-passkeys). Accept either file paths or pasted JSON. - Project ID + base URL (if not already configured) — needed for the
AuthProvider/ SDK init. - Mount point in the React app — file/component where the BYOS entry point should render.
- Existing BYOS code (if modifying) — paths to current screen components and screen map.
If the user only provides the main flow JSON, stop and ask for subflows before generating components — missing subflow JSONs is the #1 cause of [byos] no handler for screen "..." runtime errors.
Workflow
- Collect flow JSONs from the user. Agent cannot reach the Descope console — user must export and paste/path them in. Need the main flow AND every subflow it invokes (LoadSubflow actions with
arguments.flowId). This includes post-auth promotion subflows that run AFTER alogged-inaction (e.g.add-passkeys) — easy to miss because the user is already authenticated by then. If only the main flow was provided, scan it forLoadSubflowactions and ask the user to export each one before proceeding. Missing subflow JSONs → undiscovered screens →[byos] no handler for screen …at runtime.
- Parse with
parse-flow.mjs(in this directory). Runnode parse-flow.mjs— it prints every screen task withscreenName,allInputKeys,contextKeys, next-rules (interactionId → taskId), UI node summaries (inputnameattrs, button labels), and subflow invocations.
- Build the screen-name map. One React component per unique screen name across all flows. Watch for collisions — multiple tasks often share the name (e.g., "Welcome Screen" used for email-entry AND password-entry). When collisions exist, write a router component that dispatches based on
state.context.form.*heuristics (see Gotchas → Screen name collisions).
- Write each component. Every BYOS screen does the same four things:
- Read context for display:
state.context.sentTo.maskedEmail,state.context.form.email, etc. - Collect inputs into
form(viasetForm({ ...form, key: value })) - Fire
state.next(interactionId, payload)on button click - Render
state.error?.textwhen Descope reports errors
- Wire the flow. The Descope React SDK provides `
from@descope/react-sdkas the BYOS entry point —onScreenUpdatereceives(screenName, state, next)and you dispatch to the matching screen component. A common pattern is to build a thinFlowOrByoswrapper that takes abyosScreensmap and dispatches internally, butFlowOrByosis not an SDK export — you build it. Mount one instance at the entry point. OnonSuccess`, invalidate any auth-state caches (see Gotchas → Session stickiness).
- Verify end-to-end. Walk every user journey the flow supports. Any screen that hits
[byos] no handler for screen "..."in the console is missing from your map.
Critical Rules
> Note: Several rules below (nested ` tag behavior, WebAuthn ceremony ownership, ctxKey prefill, componentsConditions` field name, E.164 silent rejection) are empirically derived from real BYOS sessions and are not explicitly documented in Descope's official docs — but have been validated in production and verified against flow JSON structure.
- Form keys match the input node's
nameprop, NOTallInputKeysorinputsMetadata.key. Task 40's Set Password input hasname="newPassword"butallInputKeys: ["newPassword_noPolicyOverrides"]. Submit with{ newPassword: "..." }. - Send only the current screen's outputs in
state.nextpayloads. Spreading the full accumulated form across subflow boundaries can pollute context and cause silent failures. - **Never nest `
tags.** Descope's web component wraps children in a. Use+onClick+onKeyDown={makeEnterHandler(submit)}`. - OAuth buttons must set
providerin the payload ({ provider: 'google' }), not only the interaction ID. - Phone numbers need E.164 format. Non-E.164 gets silently rejected by the SMS connector — the flow completes but no session.
- Invalidate auth caches on
onSuccess. Session upgrades (anonymous → verified) can reuse the samesub, so userId-keyed caches never auto-refresh. - Read
componentsConditionsfrom screen JSON to mirror hide/show rules (e.g., hide "Sign in with code" whenunauthUser.verifiedPhoneis false). - WebAuthn flows: SDK runs the ceremony. When an interaction routes to a webauthn action task (
webauthn-update-user-start/finish,Sign Up or In / Passkeys), just firestate.next(interactionId). Never callnavigator.credentials.create/getfrom BYOS. Never import@simplewebauthn/browser,startAuthentication,startRegistration, or any WebAuthn helper library — the Descope SDK already does both ceremony halves. SDK observes the action ononScreenUpdateand runs the ceremony itself; cancel/error surfaces asstate.error.text. - Read input
ctxKeyfor prefill. When an input node hasprops.ctxKey="someKey", seedform[name]fromstate.context[someKey]on first render viauseEffectkeyed on the context value (only set if local field is empty). - Mirror
Device Not Supportedbranches. WebAuthn flows commonly branch ondeviceInfo.webAuthnSupport. The unsupported branch is a real screen task — give it a BYOS component, otherwise hosted widget renders mid-promotion.
Heuristics for Shared Screen Names
When two tasks share a screen name, you cannot guess a disambiguation signal. You must trace each path and build a "form field accumulation" table.
The process (non-negotiable — skipping this produces silent bugs):
- For each task that uses the colliding screen name, trace backwards through
next.rulesuntil you hit the parent flow's entry point. - For each path, list every screen task along the way and its
allInputKeys— those are the form fields the user writes into on that path. - Find a form field that is populated on exactly one path.
- Use
Boolean(state?.context?.form?.)as the heuristic.
Common mistake: picking a field that's populated on both paths because you didn't trace both paths fully.
> Examples below are from one specific flow set — your task IDs, screen names, and subflow names will differ. They illustrate the method; always run parse-flow.mjs on your own flows. > > Example: for "Verify OTP" shared between sign-in-sms-otp and progressive-profile-sms, form.phone looks attractive — but BOTH flows collect phone in a Phone-input screen before reaching Verify OTP. Pick form.password instead: the sign-in-sms-otp path is entered from the parent flow's password screen which writes form.password; the progressive-profile-sms path is entered post-magic-link where no password was ever typed.
Reference heuristics (example from the flow set this skill was born from — not universal):
| Collision | Path A collects before this screen | Path B collects before this screen | Signal | |-----------|------------------------------------|------------------------------------|--------| | "Welcome Screen" (email-entry vs password-entry) | — | email | state.context.form.email | | "Verify OTP" (sign-in-sms-otp vs progressive-profile-sms) | email, password, phone | email, phone | state.context.form.password (NOT phone — both collect it!) |
Three-way collision example (Magic Link Sent across main and two subflow variants):
| Path | Collects before this screen | | |------|----------------------------|---| | Main flow | email | no password | | Subflow new-user | email, password, fullName | password + fullName | | Subflow existing-user | email, password | password, no fullName |
Predicate: supportsChooseOther = !hasPassword || hasFullName.
Document the chosen heuristic at the top of the router component with the full trace. If the Descope console could rename one screen, ask — unique names beat heuristics forever.
Real success case: Two User Information tasks (verify-email-magic-link existing-user vs new-user branches) were renamed in console to User Information - Unverified - Email Only and User Information - Unverified - Email and Name. Heuristic was possible (presence of fullName output); rename was cheaper, clearer, and survives flow edits. Default toward rename.
Gotchas Reference
Full failure catalog (problem → symptom → fix): see references/gotchas.md. Before blaming BYOS code for a silent failure, scan that file — most "doesn't work" symptoms match a known gotcha.
Verification
End-to-end test each user journey the flow supports:
- Flow JSON says the main flow starts at
task Nand ends attask Mwithaction=logged-in? - Every screen task in the flow has either a BYOS component OR a conscious "fallback to hosted" decision?
onSuccessfires and the app reaches the expected authenticated state (not stuck anonymous)?onErrorfires and surfaces the Descope error message (not silent)?- Post-auth promotion subflows (passkey promotion, etc.) walked end-to-end including the
Device Not Supportedbranch? - WebAuthn screens fire
state.next(interaction)only — nonavigator.credentials.*calls in BYOS code?
Console should show no [byos] no handler for screen "..." warnings during a full journey walk.
Parser
parse-flow.mjs (this dir) — Node script. Input: flow JSON path(s). Output: a summary table per flow showing screen tasks, their inputs, their exit interactions, their UI node names, and subflow loaders. Run it, paste the output into your screen-map comment header, and you have the source of truth for every constant the BYOS components need.
Red Flags
If you find yourself:
- Hardcoding an interaction ID you didn't see in the flow JSON — stop, parse first
- Spreading
{ ...form }intostate.nextacross a subflow boundary — send only the outputs - Adding the same button to every "Verify OTP" variant without checking each flow's rules — gate by context
- Writing a `
tag inside your BYOS component — **use`** - Calling
navigate()inonSuccesswithout invalidating auth caches — call invalidate first - Calling
navigator.credentials.create/getfrom a BYOS click handler — the SDK does it; just fire the interaction - Importing
@simplewebauthn/browseror any WebAuthn helper — same trap, third-party-lib variant; SDK does it - Skipping subflow export because "the user is already logged in by then" — post-auth promotion subflows render real screens
All of these mean: pause, re-read references/gotchas.md, verify against the flow JSON.
References
references/byos-component-patterns.md— positive code patterns: core wiring (onScreenUpdate→ByosState), screen router, screen skeleton, and complete examples for email, OTP, phone, OAuth, collision router, and ctxKey prefill. Start here when bootstrapping.references/gotchas.md— failure catalog: 19 real BYOS failure modes, each with symptom → root cause → fix. Also contains a pre-ship checklist.parse-flow.mjs— Node parser:node parse-flow.mjs [more.json ...]. Prints screen tasks, real form-keynameprops, interaction IDs, subflow loaders, and collision warnings. Run before writing any component.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: descope
- Source: descope/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.