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.
| Workflow | Recommended TTL | Why |
|---|---|---|
| Synchronous chat tool call | 30 – 120 s | Short enough that the agent does not feel stuck; long enough for a focused user. |
| Foreground approval (transfer, action) | 5 – 15 min | Lets the user attend to it; still expires before the underlying context goes stale. |
| Background / overnight batch | 1 – 12 hours | The 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
WaitForDecisionOutcomeTimeoutErroraftermaxWaitMs; 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
RouteDecisioncall decrements quota once, regardless of how many devices receive the encrypted ciphertext. NO_DEVICESstill 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
titleandsummarythe 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
| Item | Done when… |
|---|---|
| Secrets in a vault | APP_TOKEN never appears in source; rotation is a 1-step operation. |
| Stable request ids | Retries do not produce duplicate prompts on devices. |
| Sensible TTL per workflow | Synchronous prompts use seconds, async use minutes/hours. |
| EXPIRED handled explicitly | Your agent has a documented fallback (escalate, retry, abort). |
| Quota monitoring | You alert before you hit zero, not after. |
| Client onboarding | First-run flow registers the device before the first decision is sent. |
| Multi-device tested | Approve on one surface, watch the other retract. |
| Outcome metrics | Dashboards split by approved / rejected / expired / error. |
Anti-patterns
- Don't loop on
getDecisionOutcomeat a fixed interval — use the SDK's backoff helper. - Don't bake
APP_TOKENinto 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.