Quotas
Quotas control the composition of your sample by capping how many responses are collected for each demographic segment. They prevent over-collection in easy-to-reach groups and ensure your final dataset matches your target proportions.
How quotas work
Each quota has three parts:
| Part | Example | Description |
|---|---|---|
| Name | "Males 25-34" | Human-readable label for reporting |
| Target | 250 | Number of completes needed |
| Definition | "Q2 == `1` AND Q5 IN [`3`, `4`]" | DSL condition that determines which responses count. null for a total quota |
When a respondent submits, the system evaluates every active quota's definition against their answers. If the response matches a quota and that quota is full, the response is rejected as over-quota.
Enforcement architecture
Quotas are enforced atomically using a two-tier system:
- Redis holds live counters for sub-millisecond check-and-increment during response ingestion. A Lua script checks all applicable quotas atomically — if any quota is full, none increment, preventing race conditions.
- PostgreSQL is the source of truth for quota definitions and historical counts.
- Reconciliation syncs PostgreSQL counts to Redis every 5 minutes, correcting any drift from failures or manual exclusions.
The system fails open: if Redis is unavailable, responses are accepted and reconciliation corrects the counts later.
Total vs conditional quotas
Total quota
A quota with definition: null counts every complete response regardless of answers. Use this to cap your overall sample size.
{
"name": "Total",
"target": 1000,
"definition": null
}
Conditional quota
A quota with a DSL condition string counts only responses that match. The DSL syntax is the same as skip logic conditions.
{
"name": "Males 25-34",
"target": 250,
"definition": "Q2 == `1` AND Q5 IN [`3`, `4`]"
}
Common quota structures
Gender quotas:
[
{"name": "Total", "target": 1000, "definition": null},
{"name": "Male", "target": 500, "definition": "Q2 == `1`"},
{"name": "Female", "target": 500, "definition": "Q2 == `2`"}
]
Nested quotas (gender x age):
[
{"name": "Total", "target": 1000, "definition": null},
{"name": "Males 18-34", "target": 250, "definition": "(Q2 == `1`) AND (Q3 IN [`1`, `2`])"},
{"name": "Males 35-54", "target": 250, "definition": "(Q2 == `1`) AND (Q3 IN [`3`, `4`])"},
{"name": "Females 18-34", "target": 250, "definition": "(Q2 == `2`) AND (Q3 IN [`1`, `2`])"},
{"name": "Females 35-54", "target": 250, "definition": "(Q2 == `2`) AND (Q3 IN [`3`, `4`])"}
]
Regional quotas:
[
{"name": "Total", "target": 600, "definition": null},
{"name": "Northeast", "target": 150, "definition": "Q4 IN [`1`, `2`, `3`]"},
{"name": "Southeast", "target": 150, "definition": "Q4 IN [`4`, `5`, `6`]"},
{"name": "Midwest", "target": 150, "definition": "Q4 IN [`7`, `8`, `9`]"},
{"name": "West", "target": 150, "definition": "Q4 IN [`10`, `11`, `12`]"}
]
Setting quotas
Bulk replace (recommended for initial setup)
Replace all quotas at once. This deactivates any existing quotas and creates new ones.
curl -X PUT https://surveys.flashpoint.ai/api/v1/surveys/{survey_id}/quotas \
-H "X-Service-Token: $TOKEN" \
-H "X-Team-ID: $TEAM_ID" \
-H "X-User-ID: $USER_ID" \
-H "Content-Type: application/json" \
-d '{
"quotas": [
{"name": "Total", "target": 1000, "definition": null},
{"name": "Male", "target": 500, "definition": "Q2 == `1`"},
{"name": "Female", "target": 500, "definition": "Q2 == `2`"}
]
}'
Response (200 OK):
{
"total_target": 1000,
"total_collected": 0,
"overall_fill_percentage": 0.0,
"all_quotas_met": false,
"quotas": [
{
"id": "a1b2c3d4-...",
"name": "Total",
"target": 1000,
"current_count": 0,
"fill_percentage": 0.0,
"remaining": 1000,
"is_full": false,
"is_active": true,
"definition": null,
"created_at": "2026-05-26T10:00:00Z",
"updated_at": "2026-05-26T10:00:00Z"
}
]
}
Agent prompt: "Set quotas: 1000 total, 500 males (Q2 option 1), 500 females (Q2 option 2)"
Add a single quota
Add one quota without touching existing ones.
curl -X POST https://surveys.flashpoint.ai/api/v1/surveys/{survey_id}/quotas \
-H "X-Service-Token: $TOKEN" \
-H "X-Team-ID: $TEAM_ID" \
-H "X-User-ID: $USER_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "California",
"target": 100,
"definition": "Q4 == `5`"
}'
Response (201 Created):
{
"id": "e5f6g7h8-...",
"name": "California",
"target": 100,
"definition": "Q4 == `5`"
}
Agent prompt: "Add a regional quota for California respondents, target 100, based on Q4 option 5"
Update a quota
Change a quota's name or target. Only the fields you provide are modified.
The agent tool update_quota accepts quota_id, and optionally name and/or target.
Agent prompt: "Increase the female quota to 600"
Delete a quota
Soft-deletes a quota (sets is_active = false). Redis counters are removed immediately.
Agent prompt: "Delete the California quota"
Monitoring progress
Get quota summary
Returns live fill counts from Redis for every active quota.
curl https://surveys.flashpoint.ai/api/v1/surveys/{survey_id}/quotas \
-H "X-Service-Token: $TOKEN" \
-H "X-Team-ID: $TEAM_ID" \
-H "X-User-ID: $USER_ID"
Response:
{
"total_target": 1000,
"total_collected": 347,
"overall_fill_percentage": 34.7,
"all_quotas_met": false,
"quotas": [
{
"id": "a1b2c3d4-...",
"name": "Total",
"target": 1000,
"current_count": 347,
"fill_percentage": 34.7,
"remaining": 653,
"is_full": false,
"is_active": true,
"definition": null,
"created_at": "2026-05-26T10:00:00Z",
"updated_at": "2026-05-26T10:00:00Z"
},
{
"id": "b2c3d4e5-...",
"name": "Male",
"target": 500,
"current_count": 189,
"fill_percentage": 37.8,
"remaining": 311,
"is_full": false,
"is_active": true,
"definition": "Q2 == `1`",
"created_at": "2026-05-26T10:00:00Z",
"updated_at": "2026-05-26T10:00:00Z"
},
{
"id": "c3d4e5f6-...",
"name": "Female",
"target": 500,
"current_count": 158,
"fill_percentage": 31.6,
"remaining": 342,
"is_full": false,
"is_active": true,
"definition": "Q2 == `2`",
"created_at": "2026-05-26T10:00:00Z",
"updated_at": "2026-05-26T10:00:00Z"
}
]
}
Agent prompt: "Show me the quota progress for this survey"
Export quota report
The agent's export_quota_report tool returns quota progress combined with response analytics (total responses, completes, completion rate, median time). Use it for fieldwork status updates.
Agent prompt: "Give me a fieldwork status report with quota progress"
Response shape:
{
"survey_id": "...",
"analytics": {
"total_responses": 412,
"completes": 347,
"completion_rate": 84.2,
"median_time_ms": 245000
},
"quota_summary": {
"total_target": 1000,
"total_collected": 347,
"overall_fill_percentage": 34.7,
"all_quotas_met": false
},
"quotas": [
{
"name": "Total",
"target": 1000,
"current_count": 347,
"fill_percentage": 34.7,
"remaining": 653,
"is_full": false
}
]
}
Reconciliation
Reconciliation recounts all quotas from actual response data in PostgreSQL and syncs the counts to Redis. This corrects drift caused by:
- Response exclusions (manually removing a response after it was counted)
- Redis failures during ingestion (the system fails open, so responses are accepted)
- Manual data corrections
Automatic reconciliation
Runs every 5 minutes as a background job. No action required.
Manual reconciliation
Trigger a recount on demand after excluding responses or if counts look off.
curl -X POST https://surveys.flashpoint.ai/api/v1/surveys/{survey_id}/quotas/reconcile \
-H "X-Service-Token: $TOKEN" \
-H "X-Team-ID: $TEAM_ID" \
-H "X-User-ID: $USER_ID"
Response:
{
"status": "reconciled"
}
Agent prompt: "Reconcile the quotas — I just excluded some test responses"
The agent's reconcile_quotas tool returns the updated counts after reconciliation:
{
"reconciled": true,
"total_collected": 340,
"quotas": [
{"name": "Total", "target": 1000, "current_count": 340},
{"name": "Male", "target": 500, "current_count": 185},
{"name": "Female", "target": 500, "current_count": 155}
]
}
Events
The quota system emits events that can drive notifications and automations:
| Event | Trigger | Use case |
|---|---|---|
QUOTA_UPDATED | Quotas are bulk-replaced | Audit trail |
QUOTA_WARNING | A quota reaches 80% fill | Alert the team to adjust panel sources |
QUOTA_REACHED | A quota hits its target | Close a panel source, notify stakeholders |
Best practices
-
Always include a total quota. Without one, there is no cap on overall sample size.
-
Set conditional quotas after building questions. Quota definitions reference question labels, so the questions must exist first.
-
Use the same DSL syntax as skip logic. The condition language is identical — see Skip logic & conditions for the full reference.
-
Over-recruit slightly. Set targets 5-10% above your analysis plan to account for exclusions and quality checks.
-
Monitor fill rates during fielding. Use
get_quota_progressor the agent's fieldwork report to check if any cells are filling too slowly. -
Reconcile after bulk exclusions. If you exclude a batch of responses (speed checks, quality flags), run reconciliation so the counts reflect the remaining valid data.
-
Keep quotas simple. Each additional conditional quota adds a Redis key check during ingestion. For typical surveys (under 50 quotas), this has no measurable impact. For very large quota matrices, consider collapsing into fewer conditions.
API reference
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/surveys/{survey_id}/quotas | Get quota summary with live counts |
POST | /api/v1/surveys/{survey_id}/quotas | Create a single quota |
PUT | /api/v1/surveys/{survey_id}/quotas | Bulk replace all quotas |
POST | /api/v1/surveys/{survey_id}/quotas/reconcile | Manual reconciliation |
All endpoints require X-Service-Token, X-Team-ID, and X-User-ID headers.
Next steps
- Write conditions for quotas: Skip logic & conditions
- Learn about all question types: Question types
- Publish and field the survey: Lifecycle