I IntelliAuth Docs

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 scaffoldpnpm dlx @intelliauth/action-cli init to scaffold a project; upload to 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:

ts
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

ts
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

ts
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

ts
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.log and console.error output is captured per-run and visible in the audit + flow-run detail pages. Cheap observability.

See also