Flows custom code
A custom code step runs your JavaScript during sign-in. It can refuse the sign-in or store metadata on the player. It is a paid add-on and is off until you enable it for a project.
Enable the add-on
Custom code steps need the Custom code steps add-on for the project. Until the project has it, the flow editor doesn't offer the step and shows a link to billing instead. Owners and admins enable it on the billing page with Enable custom code. See theadd-on reference for the price and allowance.
Add a step
- In Settings, select Open the flows storyboard and open a flow.
- Under New step, choose Custom code, then select Add step.
- Write your code in Code. Set Timeout (ms) and choose what happens under If the project no longer has the add-on.
- Select Apply to draft, then Save draft.
- Use Run test under Test run to try it, then Publish draft.
Owners and admins change steps and run real test runs. Viewers can read the code, and their test runs treat custom code steps as continuing without running them.
Write the code
The code is a JavaScript module. Its default export is called with the event and an object that holds the step's secrets, and returns what should happen next.
// Refuse players your own service has banned, and store their tier.
export default async function (event, { secrets }) {
const { user_id: userId, method } = event.input;
if (!userId || method === "guest") return { action: "continue" };
const response = await fetch("https://api.example.com/players/" + userId, {
headers: { Authorization: "Bearer " + secrets.PLAYER_API_KEY },
});
if (!response.ok) throw new Error("Player service answered " + response.status);
const player = await response.json();
if (player.banned) {
console.log("Refused a banned player");
return { action: "deny", reason_code: "banned_player", message: "account_restricted" };
}
return { action: "continue", claims: { tier: player.tier } };
}The event has the trigger, the application, the sign-in method and the edge country. After sign-up it also has the player's user id. Nothing else from the sign-in request is included: no IP address, email address, token or header. The event can't be changed.
{
"type": "flow.custom_code",
"version": 1,
"step": "banned",
"sent_at": "2026-10-07T10:00:00.000Z",
"input": {
"version": 1,
"trigger": "post_login",
"tenant_id": "ten_…",
"application_id": "cli_…",
"user_id": "usr_…",
"method": "steam",
"context": { "country": "ZA", "provider": "steam" }
}
}Return one of these:
{ "action": "continue" }
{ "action": "continue", "claims": { "tier": "gold" } }
{ "action": "deny", "reason_code": "banned_player", "message": "account_restricted" }claims stores up to 16 keys in the player's metadata, which appears in theauth4_meta token claim from the next token on. null removes a key. Claims are written only when the whole flow continues. A denial stops the flow; hosted login shows the fixed message you name, and the reason code appears only in traces.
Anything else, an error your code throws, or running past the timeout fails the step. The step's If the step fails or times out setting then decides: deny the sign-in or pass over the step.
Limits
| Limit | Value |
|---|---|
| Code size per step | 12 KB |
| Custom code steps per flow | 4 |
| Timeout per step | 50 to 1,900 ms, and within the flow's time budget |
| CPU time per run | 50 ms |
| Outgoing requests per run | 5 |
| Secrets per step | 8, each up to 1 KB |
| Answer size | 4 KB |
Your code can call fetch() on public http and https addresses on the default ports. Requests to private or internal addresses, to localhost and names such as .internal or .local, and to auth4.dev hosts are refused with status 403. Redirects are returned to your code rather than followed. The code has no access to storage or other auth4.dev services.
Secrets
Keep API keys out of the code. Open the step after the draft is saved and add them under the step's secrets with Secret name, Secret value and Save secret. Names use capital letters, digits and _. Your code reads them assecrets.NAME.
Values are stored encrypted and can't be shown again; to change one, save it again under the same name. Secrets belong to the step id, so they apply to every version of the flow and a new value takes effect at once. If you rename the step, add its secrets again.
Test runs and logs
A test run runs the draft against a sample event without storing anything or writing a trace. The result shows the outcome, the reason code and each step's trace.
What your code writes with console.log, and errors it throws, appear under Custom code logs in the flow editor, newest first, for 7 days. Lines can take a minute to appear. Secret values are replaced with [redacted], but don't log anything you wouldn't want every member of the project to read.
If the add-on ends
The add-on is checked every time a flow runs. If the project no longer has it, custom code steps don't run. Each one follows its If the project no longer has the add-on setting: deny the sign-in, or pass over the step and carry on with the next one. Every skipped step is recorded in the project's audit log, and the trace shows it as skipped because the add-on is off.
While the add-on is off you can't save, publish or make active a version with custom code steps. You can remove the steps and publish the flow without them, or delete their secrets.