If a platform template doesn't cover your case, write a custom Action. Three options for how:
- In-console editor — TypeScript editor inside the admin console. Best for short Actions and rapid iteration.
- External package — author locally with your editor of choice, package as a tarball, upload via the console. Best for substantial Actions with their own tests.
- CLI scaffold —
pnpm dlx @intelliauth/action-cli initto scaffold a project;uploadto publish. Best for actions in CI/CD.
This guide walks the in-console editor. The other paths produce identical Actions; the workflow is just different.
1. Open the editor
Section titled “1. Open the editor”Authentication → Actions → New action.
Pick a starter:
- Empty — bare-bones skeleton.
- Domain allowlist — pre-filled with the "reject sign-ins outside a domain list" pattern.
- Webhook emit — pre-filled with the "post to a URL" pattern.
- Custom claim — pre-filled with the "add a claim to the token" pattern.
Picking a starter copies its code as a baseline; you edit from there.
2. Edit the metadata
Section titled “2. Edit the metadata”Top of the editor — metadata panel:
- Name — what tenant admins see in the Actions list ("Block by email domain").
- Slug — URL-safe identifier ("block-by-email-domain"). Immutable once saved.
- Version — semver string ("0.1.0"). Auto-bumps on save; you can override.
- Compatible triggers — which flow trigger slots this Action can attach to (e.g.,
login.pre-credential-check,registration.pre-create). Multi-select. - Config schema — the form tenant admins fill when attaching this Action to a flow. Defined as JSON Schema (the editor has a visual schema-builder for non-developers).
3. Write the code
Section titled “3. Write the code”The code editor is the bulk of the page. Default starting skeleton:
export async function execute(event, api) { // event.* — read-only context: user, request IP/country/VPN flags, OAuth client, etc. // api.* — side effects: api.access.deny(), api.accessToken.setCustomClaim(), api.fetch() // Use the {} picker in the editor to browse every available event.* path.
return api.access.allow()}The full programming model (event shape, api methods, sandbox constraints) is in the Custom actions overview and the API reference. For most Actions you'll only use a small slice of it.
4. Test in the editor
Section titled “4. Test in the editor”The editor has a Test tab. Fill in synthetic inputs (a fake user, a fake request, a fake config); click Run; see the output. Useful for confirming the logic works before pushing to live flows.
5. Save + publish
Section titled “5. Save + publish”Click Save. The Action is published at the version you set. Available in the Actions list immediately, and attachable via the Flow builder.
The publish action records flow.action_published in audit.
6. Attach to a flow
Section titled “6. Attach to a flow”Now the part that makes it run. Authentication → Flows → pick a flow → trigger slot → Add Action → pick yours → configure → save.
The new Action runs on the NEXT flow execution (next sign-in / signup / etc., depending on the flow).
Common patterns
Section titled “Common patterns”Block by email domain
Section titled “Block by email domain”export async function execute(event, api) { const email = event.user?.email if (!email) return api.access.allow() // social sign-in: skip
const domain = email.split('@')[1]?.toLowerCase() const allowed = event.client.metadata?.allowed_domains ?? []
if (allowed.includes(domain)) { return api.access.allow() }
return api.access.deny( `Sign-in restricted. Use an email at one of: ${allowed.join(', ')}.` )}Attach to the Pre-Login Stage of the Login flow.
Webhook on signup
Section titled “Webhook on signup”export async function execute(event, api) { const { id, email, name } = event.user
await api.fetch(event.client.metadata.webhook_url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event: 'user.signed_up', user_id: id, email, name, }), })
return api.access.allow()}Attach to the Post-Create Stage of the Registration flow.
Decorate token with custom claim
Section titled “Decorate token with custom claim”export async function execute(event, api) { const tier = event.user.app_metadata?.tier ?? 'free' api.accessToken.setCustomClaim('https://cymmetri.com/tier', tier) return api.access.allow()}Attach to the Post-Login Stage of the Login flow. The claim ends up on the issued access token.
Production tips
Section titled “Production tips”- Keep Actions short. Per-Action time budget is a few seconds. Slow Actions slow sign-in for every user.
- Don't call slow external APIs synchronously. If you must, set a short timeout + fail-open (return continue on timeout, log the failure).
- Test before attaching. The flow-builder test feature lets you simulate a sign-in through the flow with your Action attached. Use it.
- Version intentionally. Don't ship v0.1.0 → v0.1.1 with breaking config schema changes; the change in schema breaks every attached flow.
- Log freely.
console.logandconsole.erroroutput is captured per-run and visible in the audit + flow-run detail pages. Cheap observability.