Documentation

Actions

Your own code at points of the approval pipeline: alerts, step-ups and refusals by country, local time, role or amount, passkeys for admins. Start from a no-code template or a few lines of JavaScript (whatever your server language), replay it against your real recent decisions, then deploy, watch and roll back.

Triggers
TriggerWhenDecisions
login.requestBefore the push is sent (POST /login/request), after the risk engine and your rulesdeny, stepUp, requirePasskey · skipNumberMatch*, overrideBlock*
risk.checkSilent sign-in decision (POST /risk/check)prompt · allowSilent*
agent.approvalBefore an AI agent's request reaches the phone (POST /agents/approvals): event.agent, event.request.authorizationDetails, amounts, destinationsdeny, stepUp, requirePasskey (only tightening)
approval.decidedApproved or rejected (phone, passkey, offline QR, recovery code)—
shield.decisionVishing Shield changed an approval made during a call; re-confirmed or taken back—
fraud.reportedYour server reported a request as fraud—
device.linked · device.keys_lost · user.unlinkedPhone linked; its keys were destroyed; 2FA turned off from the phone—
passkey.added · passkey.removed · recovery.updatedPasskeys; Never Locked Out recovery steps—
identity.pre_user_registrationTouchque Login: before a customer signs up (address and name checked, nothing created yet)deny
identity.post_loginTouchque Login: after a customer's first factor, before the second step and the app's codedeny, requireMfa · idToken.setCustomClaim, accessToken.setCustomClaim, user.setAppMetadata, user.setUserMetadata
identity.post_user_registrationTouchque Login: a customer signed up—

Every Action can also notify (api.notify.slack | teams | webhook | email), call its allowed hosts (api.fetch) and freeze the user (api.freezeUser). * needs a capability.

Tighten freely, loosen only with the phone. deny, stepUp, requirePasskey, prompt, notifications and freezeUser always count. A loosening call counts only when the version declares the capability and an owner or admin approved that version on their phone (dashboard → Actions); until then it is pending_approval and the previous version keeps running.
CapabilityWhat it allows
skip_number_matchDrop the number the workspace itself asked for: the always-challenge setting or an access rule step-up. Never the risk engine's asks, critical actions, push fatigue, a flagged phone or an engine outage.
override_blockTurn a risk-engine refusal into a number-matched push (never a plain one; never a country block, an access-rule refusal or a frozen account).
allow_silentLet a known, device-bound browser approved on the phone linked now skip the phone when only a new network or country, or an older approval (at most 30 days, never past your maxAgeSeconds), made it ask. Never for critical actions, a Vishing Shield signal, a workspace gate, an engine outage or a verdict other than ALLOW.
Sandboxed: QuickJS in WebAssembly, a fresh runtime per run, no filesystem or process, network only through api.fetch to the version's allowedHosts. 50 ms CPU, 16 MB, 300 ms for all Actions of a pre trigger. Failures never block a sign-in unless onFailure is step_up or deny. Secrets are encrypted, write-only and reach only versions that declare them.
Developer loop
EndpointDoes
POST /actions/testOne run on a recorded event, your own event or a sample: decisions, notifications (not sent), console output, and what the decision would have been.
POST /actions/dry-runReplays a draft against the last (up to 1000) real decisions: how many would change and how (push→number_match, push→deny…), notifications it would send, errors, p95.
PUT /actions/{slug}Deploy (create or new version, idempotent). POST …/rollback is one call.
GET /actions/{slug}/logs · /metricsEvery live run, runs / errors / p95 / decisions per window.

An Action's reason is part of the decision's explanation: { code: "action", text, action } in risk.check reasons and step-ups, and a refusal answers 403 { code: "blocked", reason: "action", action, error }. Pass what only your server knows as context on the login request or the guard (never shown on the phone).

No-code templates
idTriggerDoes
alertanySlack, Teams, webhook or e-mail (Resend, Postmark, SendGrid) when the event matches
block_or_step_uplogin.requestdeny, stepUp or requirePasskey by country, local time, role, amount, action or risk
require_passkey_for_rolelogin.requestOnly a passkey approves for these roles (context.roles)
always_ask_whenrisk.checkNever silent when the sign-in matches

when: countries, notCountries, hours { from, to, timeZone }, roles, actions, criticalOnly, riskAbove, amount { path, over }, status. Server SDKs 5.2.0 (action types moved to tq.actionTypes in 4.0).

Deploy, replay, roll back
// A template: Slack for approvals from Turkey at night
await tq.actions.secrets.set('SLACK_WEBHOOK_URL', ({}).SLACK_WEBHOOK_URL);
await tq.actions.deploy('night-alert', {
  trigger: 'approval.decided',
  template: { id: 'alert', params: { channel: 'slack',
    when: { countries: ['TR'], hours: { from: 0, to: 6, timeZone: 'Europe/Istanbul' }, status: 'approved' } } },
});

// Your own code, typed; replay it against the last 500 real decisions first
const handler = async (event, api) => {
  if (event.context?.roles?.includes('admin')) api.requirePasskey('Admins approve with a passkey');
};
const report = await tq.actions.dryRun({ trigger: 'login.request', handler });
// report.transitions → { 'push→passkey': 12 }
await tq.actions.deploy('admins-use-passkeys', { trigger: 'login.request', handler });

// Watch, roll back
await tq.actions.logs('admins-use-passkeys');
await tq.actions.rollback('admins-use-passkeys');
An Action
// login.request
exports.onExecute = async (event, api) => {
  const t = api.time.local(event.client.timeZone || 'Europe/Istanbul');
  if (event.client.country === 'TR' && t.hour < 6) {
    api.stepUp('Night sign-in from Turkey');
    api.notify.slack(`${event.user.id} is signing in from ${event.client.location}`);
  }
};
Dry run 200
{
  "trigger": "login.request",
  "evaluated": 500,
  "matched": 41,
  "changed": 38,
  "transitions": { "push→number_match": 35, "number_match→deny": 3 },
  "notifications": { "slack": 41 },
  "errors": 0,
  "timeouts": 0,
  "p50Ms": 2,
  "p95Ms": 6
}