Limits & quotas
Limits & quotas
Every cap, quota, and rate limit the Telenow App Platform enforces — with the exact number, the HTTP status you get at the cap, and whether the value is rejected (the request fails), clamped (silently pulled into range), or truncated (the extra data is dropped but the request succeeds).
These guardrails exist so one app's runaway loop can't starve other tenants. They are generous for legitimate apps. Caps are per (org, app) unless noted — your app sees its own counts in one org, isolated from every other install.
A note on "soft" caps: some counts (the row cap, the proxy/circuit-breaker state, the Data-API rate buckets) are process-local and cached, so on a multi-node cluster the effective limit may overshoot slightly before it settles. They stop abuse; they are not billing.
Data store & reads
| Limit | Value | At the cap | Behaviour |
|---|---|---|---|
| Records per (org, app), across all object types | 100000 (MAX_ROWS_PER_APP) | 413 Payload Too Large, message app data store is full (max 100000 records per app) | Rejected — create fails |
List default page size (when limit unset) | 100 | — | default |
| List page size range | 1..=500 | over-large → 500; 0 → the default; negative → 1 | Clamped (not errored) |
Semantic search topK / limit default | 20 | — | default |
Semantic search topK / limit range | 1..=500 | same clamp as a plain list | Clamped — on builds before mid-2026 this path was unclamped, so a negative limit errored the query and a huge topK scanned the whole vector index |
Agent object.query result rows | 50 | — | Fixed — no manifest override, no pagination; filter instead |
Expand fields per read (expand=) | 8 (MAX_EXPAND_FIELDS) | extra expand keys ignored | unknown / non-relation keys silently ignored |
| Referenced rows fetched per expand field | 500 (MAX_EXPAND_IDS) | to-many beyond 500 | Truncated |
| Computed-field rendered length | 8192 chars (MAX_COMPUTED_LEN) | over-length output | Truncated |
| Offset pagination over REST | not supported | — | use limit + sort |
The row count is soft and process-local, cached with a 60-second TTL. A cluster may overshoot the cap slightly, and freed rows (deletes) free up capacity within about one minute as the cache refreshes.
The cap covers every writer, not just the REST API: dashboard-bridge creates, agent object.create tools, inbound webhooks, workflow create-object steps, event-rule upserts, and public-link form submissions. That last one is the one to think about when you issue a form link to someone outside the org — it is the only write into your store an unauthenticated caller can reach, so the row cap is what bounds a link that gets shared further than you intended. Prefer single-use links, and keep expiries short. (On builds before mid-2026 the form path skipped the cap entirely.)
POST /api/app-data/patient
Authorization: Bearer <app key>
HTTP/1.1 413 Payload Too Large
{ "success": false, "error": "app data store is full (max 100000 records per app)" }
Blobs / files
| Limit | Value |
|---|---|
| Per file | 25 MB (MAX_BLOB_SIZE_BYTES) |
| Total per app (in one org) | 1 GB (MAX_TOTAL_BLOB_BYTES) |
| Number of files per app | 10000 (MAX_BLOBS_PER_APP) |
A put over any of these is rejected. The byte cap and the file-count cap are both enforced — a flood of tiny files is blocked by the 10,000-file cap even though it fits under 1 GB.
Data API rate limit
The programmatic app-key Data API (/api/app-data/...) is rate-limited with a token bucket, per (org, app):
| Setting | Value |
|---|---|
| Sustained rate | REFILL_PER_SEC = 20.0 (~20 req/s) |
| Burst | BURST = 60.0 |
| Over limit | 429 Too Many Requests, body retry_after_secs: 1, message Data API rate limit reached — slow down |
The in-dashboard data proxy (member-authed — a human is signed in) is not subject to this limit. Only the programmatic app-key Data API is rate-limited here.
GET /api/app-data/patient
Authorization: Bearer <app key>
HTTP/1.1 429 Too Many Requests
{ "success": false, "error": "Data API rate limit reached — slow down", "retry_after_secs": 1 }
Tools
| Limit | Value | Behaviour |
|---|---|---|
Tool timeoutSecs | 1–30 seconds, default 15 when omitted | Clamped to 1..=30 |
object.query agent tool | hard-capped at 50 rows, equality-only filters | rows beyond 50 dropped |
http tool base_url | must be public HTTPS | Rejected — no localhost / private / loopback / link-local / cloud-metadata (SSRF-guarded) |
http tool response | <= 1 MB | Rejected — app tool response exceeded 1 MB cap |
http tool non-2xx response | any non-2xx | aborts the tool |
Note:
timeoutSecsclamps to 1–30 seconds (not 1–10). A 15-second default applies when you omit it.
Sandbox runtime (js / sandbox handlers)
The hardened code runtime applies its own hard limits, independent of timeoutSecs:
| Limit | Value |
|---|---|
| Source size | <= 16 KB (MAX_SOURCE_BYTES) |
| Serialized output | <= 256 KB (MAX_OUTPUT_BYTES) |
| Heap | 16 MB (hard QuickJS cap → JS exception, never host OOM) |
| Native stack | 256 KB |
| Execution timeout | clamped 100 ms–5000 ms (independent of timeoutSecs) |
| Global concurrency | default 32 in-flight → returns status busy over the cap |
Automation
Manifest caps (validated at upload — rejected otherwise)
| Limit | Value |
|---|---|
| Schedules per app | 20 (MAX_SCHEDULES) |
| Schedule minimum interval | 300 seconds (5 min) (MIN_SCHEDULE_SECS) |
| Workflows per app | 20 (MAX_WORKFLOWS) |
| Steps per workflow | 20 (MAX_WORKFLOW_STEPS) |
| Inbound hooks per app | 20 (MAX_INBOUND_HOOKS) |
every is a simple <positive int><unit> duration — unit in {m | min, h | hr, d | day}. No seconds, no weeks, no cron. Any multiple is allowed (e.g. 90m, 6h, 2d), but anything under 5 minutes is rejected at validate.
"schedules": [
{ "key": "nightly", "every": "24h", "handler": { "kind": "rule", "object": "...", "key": "...", "map": {} } }
]
// "every": "2m" → rejected (under the 5-minute floor)
Inbound hooks
| Limit | Value | Behaviour |
|---|---|---|
| POST body | 256 KB (DefaultBodyLimit) | Rejected over the cap |
Workflow runtime
| Limit | Value | Behaviour |
|---|---|---|
| In-flight runs per (org, app) | 1000 (MAX_RUNNING_RUNS_PER_APP) | over the cap, a trigger is silently dropped |
| Step attempts | 6 (MAX_STEP_ATTEMPTS) | after 6 the run is marked failed |
| Retry backoff | 5s * 2^attempt, capped at MAX_DELAY_SECS = 7 days | — |
delay step | clamped to max 7 days | Clamped |
http step request body | <= 256 KB (HTTP_REQ_BODY_CAP) | step retries, then fails after MAX_STEP_ATTEMPTS |
http step stored response | truncated to ~8 KB (HTTP_RESP_CAP) | Truncated |
http step timeout | clamped 1–30s, default 15s | Clamped |
Step outcomes: a 5xx response or a transport error retries (backoff absorbs transient failures); a 4xx response and an update-object with no matching row are non-fatal (the step is Done).
Agents / knowledge bases / eval
Manifest caps (reject the upload otherwise)
| Limit | Value |
|---|---|
agents[] | <= 50 |
agentTeams[] | <= 20 |
| Members per team | 1..=10 (unique, key-safe refs; entry must equal a member ref) |
knowledgeBases[] | <= 20 |
| Documents per KB | <= 100 |
| Document body | <= 512 KB (title must be non-empty) |
Runtime cap
| Limit | Value | At the cap |
|---|---|---|
| Provisioned agents per (org, app) | 100 (MAX_AGENTS_PER_APP) | 413 — this app has reached its agent limit (100) for this org |
This runtime cap is enforced across every create path — dashboard create, the app-key POST /api/app-agents, and whole-team create. It is distinct from the 50 manifest-template cap (agents[]): the 50 caps how many templates a manifest may ship; the 100 caps how many real agents you may provision.
Eval
| Limit | Value | Behaviour |
|---|---|---|
| Scenarios per request | max 3 (MAX_EVAL_SCENARIOS), at least 1 required | otherwise 400 |
Per-scenario max_turns | default 4, clamped to 6 (EVAL_MAX_TURNS) | Clamped |
| Whole-request budget | 200s (EVAL_REQUEST_BUDGET_SECS) | 400 — eval timed out — try fewer scenarios or turns |
Campaigns
| Limit | Value | At the cap |
|---|---|---|
| Active campaigns per app per org (draft/running/paused) | 50 (MAX_CAMPAIGNS_PER_APP) | 400 — app campaign limit reached (50 active) |
| Targets per campaign | 5000 (MAX_TARGETS_PER_CAMPAIGN) | explicit lists truncated to 5000; targetQuery keyset-pages up to 5000 then stops |
| Create body | 2 MB | Rejected over the cap |
HTTP proxy (telenow.http / bridge)
The server-side proxy your dashboard UI calls through (telenow.http(...)). State is process-local (single node).
| Guard | Value | Over the limit |
|---|---|---|
| Per-org rate (token bucket) | REFILL_PER_SEC = 20 sustained, BURST = 40 | 429, retry_after 1s, app API proxy rate limit reached — slow down |
| Global concurrency (semaphore) | 64 in-flight | 429, retry_after 1s, proxy is at capacity — retry shortly |
| Per-host circuit breaker | trips after TRIP_AFTER = 5 consecutive transport/5xx failures, stays open OPEN_FOR = 30s | 429, retry_after 30s, upstream <host> is temporarily unavailable |
| Response size | <= 1 MB | Rejected |
| Scheme | HTTPS-only | Rejected |
A 4xx response counts as "up" — it does not trip the breaker.
Package / upload
All of these are rejections, not truncations — the whole package upload fails if any is exceeded.
| Limit | Value | At the cap |
|---|---|---|
| Whole multipart upload body | 32 MB (PACKAGE_UPLOAD_LIMIT) | 413 — package exceeds the 32 MB limit |
| Zip total uncompressed | 40 MB (MAX_TOTAL_UNCOMPRESSED) | rejected (zip-bomb guard) |
| Files in the zip | 400 (MAX_ENTRIES) | rejected |
UI bundle (ui.entry, built JS) | 8 MB (MAX_BUNDLE_BYTES) | rejected |
| UI styles (built CSS) | 2 MB (MAX_STYLES_BYTES) | rejected |
| Per screenshot | 4 MB (MAX_SCREENSHOT_BYTES) | rejected |
| Screenshots | 5 (MAX_SCREENSHOTS) | rejected |
README.md / CHANGELOG.md (each) | 64 KB (MAX_README_BYTES) | rejected |
Sandbox codeFile | 64 KB (MAX_CODE_BYTES) | rejected |
telenow.app.json | 512 KB (MAX_MANIFEST_BYTES) | rejected |
Session token & stream tickets
| Token | TTL | Notes |
|---|---|---|
| App session token | 1800s (30 min) | minted via telenow.session.token() / POST /:appId/session-token |
| Live-call / object-change stream ticket | 30s | single-use |