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
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
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
The code editor is the bulk of the page. Default starting skeleton:
export async function execute(input) {
// input.user — the user attempting sign-in (or null on signup)
// input.request — { ip, user_agent, headers, fingerprint }
// input.state — shared state across the run (read + mutate)
// input.context.config — the values the tenant admin filled in when attaching this Action
return { kind: 'continue' }
}
The full programming model (input shape, return shape, available libraries, sandbox constraints) is in the developer-facing pipelines concept. For most Actions you'll only use a small slice of it.
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
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
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
Block by email domain
export async function execute(input) {
const email = input.state.submitted_email
if (!email) return { kind: 'continue' } // social sign-in: skip
const domain = email.split('@')[1]?.toLowerCase()
const allowed = input.context.config.allowed_domains ?? []
if (allowed.includes(domain)) {
return { kind: 'continue' }
}
return {
kind: 'block',
reason: `Sign-in restricted. Use an email at one of: ${allowed.join(', ')}.`,
code: 'domain_not_allowed',
}
}
Attach to login.pre-credential-check.
Webhook on signup
export async function execute(input) {
const user = input.user
if (!user) return { kind: 'continue' }
await fetch(input.context.config.webhook_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'user.signed_up',
user_id: user.id,
email: user.email,
name: user.name,
}),
})
return { kind: 'continue' }
}
Attach to registration.post-create.
Decorate token with custom claim
export async function execute(input) {
const tier = input.user?.custom_attributes?.tier ?? 'free'
input.state.token_claims = {
...(input.state.token_claims ?? {}),
'https://cymmetri.com/tier': tier,
}
return { kind: 'continue' }
}
Attach to login.post-success. The claim ends up on the issued access token.
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.