Guides

Best Practices

Production patterns for routing decisions, handling outcomes, and keeping your agent + Noxy integration boring in the best way.

Pick the right TTL

The TTL of a decision drives almost every downstream behaviour: how long the relay holds it, how long the SDK polls, and how aggressively the device shows a reminder. Match TTL to the workflow, not to a global default.

WorkflowRecommended TTLWhy
Synchronous chat tool call30 – 120 sShort enough that the agent does not feel stuck; long enough for a focused user.
Foreground approval (transfer, action)5 – 15 minLets the user attend to it; still expires before the underlying context goes stale.
Background / overnight batch1 – 12 hoursThe agent does not wait — it polls later or wakes on outcome.

EXPIRED is a feature. Treat EXPIRED as a safe default — the user did not say yes or no, so do not assume either. Re-issue with a different TTL, escalate to a fallback user, or take the conservative path automatically.

Make every request idempotent

Generate a stable request_id per logical attempt (not per retry). The relay returns the same decision_id if you retry the same request id after a transient failure, so you can safely re-send without duplicating prompts on the user's device.

const requestId = `decision:${workflowRunId}:${stepId}`; // stable across retries
const outcomes = await client.sendDecision(identity, payload, { requestId });

Retry with backoff, not with replay

  • Retry the same request id after UNAVAILABLE, RESOURCE_EXHAUSTED / RATE_LIMITED, or network errors.
  • Use exponential backoff with jitter (e.g. 500 ms → 2 s → 6 s → 20 s).
  • Stop retrying after a budget — the SDKs raise WaitForDecisionOutcomeTimeoutError after maxWaitMs; mirror that in your own retry loop.
  • Cap concurrency under the per-app and per-connection rate limits so retries do not snowball into more RATE_LIMITED.

Respect the quota

Call getQuota() at the start of long-running batch jobs and gate the workflow on quota_remaining. If your plan is on the edge, batch fewer prompts per run or upgrade in the dashboard.

  • Each RouteDecision call decrements quota once, regardless of how many devices receive the encrypted ciphertext.
  • NO_DEVICES still consumes quota — guard the first run with a Client SDK registration check.
  • Quotas reset on your billing cycle; upgrade plan from the dashboard.

Decisions should be self-contained

  • Include a clear title and summary the client can render without extra fetches.
  • Add structured fields the client app already knows (e.g. amount, recipient) so the UI can produce a rich confirm screen.
  • Never send secrets, internal ids the user cannot map to anything, or anything you would not want to read out loud.

Handle multi-device gracefully

Outcomes are identity-wide. If a user has a phone and a laptop registered, expect:

  • The decision to appear on both surfaces.
  • The first answer to win — the other surface will see the decision retracted by the SDK.
  • Quota to be charged once per RouteDecision, not per device.

Observability

  • Log the pair (request_id, decision_id) on every routed decision; both appear in relay-side audit logs and are how you correlate end-to-end.
  • Emit a metric for each terminal outcome bucket — approved, rejected, expired, error — and alert on sudden shifts.
  • Track time-to-outcome distributions per workflow; expired-tail spikes typically mean the user-facing surface is broken before the relay.

Production checklist

ItemDone when…
Secrets in a vaultAPP_TOKEN never appears in source; rotation is a 1-step operation.
Stable request idsRetries do not produce duplicate prompts on devices.
Sensible TTL per workflowSynchronous prompts use seconds, async use minutes/hours.
EXPIRED handled explicitlyYour agent has a documented fallback (escalate, retry, abort).
Quota monitoringYou alert before you hit zero, not after.
Client onboardingFirst-run flow registers the device before the first decision is sent.
Multi-device testedApprove on one surface, watch the other retract.
Outcome metricsDashboards split by approved / rejected / expired / error.

Anti-patterns

  • Don't loop on getDecisionOutcome at a fixed interval — use the SDK's backoff helper.
  • Don't bake APP_TOKEN into client apps. Routing rights stay on the backend.
  • Don't ship a 1-second TTL "to make the agent feel snappy." It will mostly expire before the user's screen even lights up.
  • Don't change the case or format of an identity id after first registration — devices are bound to the exact string.