Questionnaires
Create, edit, and review in-app questionnaires from the Owlmetry CLI.
Questionnaires are structured multi-question surveys shown in-app via the Swift SDK's OwlQuestionnaireView or the auto-trigger .owlQuestionnaire(slug:trigger:…) view modifier. The auto-trigger opens at a small "Have a minute for feedback?" detent with three actions (take the survey, maybe later, never ask again) and expands to a step-through flow on consent — one question per page with a progress bar, non-swipe-dismissible so users finish or cancel deliberately. Each questionnaire has an immutable slug, a versioned schema of up to 30 questions (text, single/multi choice, rating, NPS), and a one-and-done global dismiss path so users who reject are never asked again. Responses store a snapshot of the schema they were submitted against, so historical answers always render correctly even after the parent definition has been edited.
Drafts are first-class responses. The SDK saves the current answer set on every Next tap (not just on Submit), so a user who quits halfway leaves a row with submitted_at = null and status = 'draft'. The next eligible launch resumes them at the first unanswered question with prior answers pre-filled (consent is skipped on resume). The final Submit flips submitted_at to non-null, snapshots the schema onto the row, transitions status to new, and fires the team notification exactly once. Per-question analytics and response lists include drafts by default so abandonment shows up naturally as a drop-off curve; pass submitted_only to scope to completed submissions. Abandoned drafts untouched for 90 days are soft-deleted by the daily questionnaire_draft_cleanup job.
The owlmetry questionnaires command group manages definitions and reads responses.
Create a questionnaire
owlmetry questionnaires create \
--project-id <id> \
--slug post-onboarding \
--name "Onboarding survey" \
--schema-file ./onboarding-survey.jsonThe --schema-file flag points at a JSON file shaped like { version: 1, questions: [...] }. You can also pass --schema '<json>' inline.
A minimal schema file:
{
"version": 1,
"questions": [
{
"id": "q_rating",
"type": "rating",
"title": "How would you rate the app?",
"required": true,
"scale": 5
},
{
"id": "q_role",
"type": "single_choice",
"title": "How would you describe yourself?",
"required": true,
"options": [
{ "id": "hobby", "label": "Hobbyist" },
{ "id": "indie", "label": "Indie developer" },
{ "id": "team", "label": "Part of a team" }
]
},
{
"id": "q_feedback",
"type": "text",
"title": "Anything you'd like to add?",
"required": false,
"multiline": true
}
]
}Slug must be lowercase letters/digits/hyphens (max 64 chars) and is immutable after creation.
text questions default to a single-line field. Set "multiline": true (as on q_feedback above) to render a tall text editor in the SDK — use it for paragraph-style answers. Both variants share the same 4000-character cap.
List questionnaires
# Single project
owlmetry questionnaires list --project-id <id>
# Every questionnaire across every accessible project in a team
owlmetry questionnaires list --team-id <id>| Flag | Description |
|---|---|
--project-id <id> | Project ID (mutually exclusive with --team-id) |
--team-id <id> | Team ID — lists across every accessible project (mutually exclusive with --project-id). Adds a Project column to the table output. |
--app-id <id> | Filter to a specific app (only with --project-id) |
--active | Show only active questionnaires |
--inactive | Show only paused questionnaires |
--data-mode <m> | production, development, all — filters the rolled-up response_count / submitted_count / last_response_at on each row. Default: all modes. |
--limit <n> | Max entries |
--cursor <cursor> | Pagination cursor (only with --project-id; team mode is LIMIT-capped) |
View definition
owlmetry questionnaires view <questionnaireId> --project-id <id>Prints the schema, response count, last-response timestamp. Pass --data-mode <production|development|all> to filter the rolled-up response_count / submitted_count / last_response_at (default: all modes mixed).
Update
# Pause a questionnaire so the SDK stops returning it as eligible
owlmetry questionnaires update <id> --project-id <pid> --active false
# Replace the schema (slug stays the same — existing responses keep their snapshot)
owlmetry questionnaires update <id> --project-id <pid> --schema-file ./v2-schema.json
# Pin to a specific app
owlmetry questionnaires update <id> --project-id <pid> --app-id <app-id>Slug cannot be updated. Passing --description "" clears the description.
Delete
owlmetry questionnaires delete <id> --project-id <pid>User-only. Agent keys get 403. Existing responses are preserved (soft delete sets deleted_at and is_active=false).
List responses
owlmetry questionnaires responses <questionnaireId> --project-id <id>Drafts and submitted responses are both listed by default — drafts carry status: "draft" and submitted_at: null. Pass --submitted-only to scope to completed submissions, or --status draft to scope to in-progress drafts only.
| Flag | Description |
|---|---|
--status <status> | Filter: draft, new, in_review, addressed, dismissed |
--submitted-only | Hide drafts; show only fully-submitted responses |
--app-id <id> | Filter to a specific app |
--dev | Dev responses only |
--data-mode <m> | production (default), development, all |
--limit <n> | Max entries |
--cursor <cursor> | Pagination cursor |
View a response
owlmetry questionnaires response <responseId> --project-id <pid> --questionnaire <qid>Renders each question (from the response's frozen schema_snapshot) alongside the answer plus any comments.
Update response status
owlmetry questionnaires status <responseId> \
--project-id <pid> --questionnaire <qid> --to in_reviewTarget status accepts draft, new, in_review, addressed, or dismissed. A draft normally transitions to new automatically when the user taps Submit in the SDK; you'd typically only set --to draft to undo a triage transition while the row is still unsubmitted.
Add a comment
owlmetry questionnaires comment <responseId> \
--project-id <pid> --questionnaire <qid> \
--body "Cross-linked to issue #321"Analytics
owlmetry questionnaires analytics <questionnaireId> --project-id <id>Pre-aggregated distribution per question:
- text → 10 most recent answers
- single_choice / multi_choice → bar list with counts + %
- rating → 1–5 buckets + average
- nps → 0–10 buckets + score + detractor/passive/promoter split
Drafts contribute to the rollups by default — a Q1-only draft adds to Q1's count and naturally drops out of Q2+, so abandonment shows up as a drop-off curve across the question list. Pass --submitted-only to compute rollups against fully-submitted responses only. The summary line always surfaces submitted_count of total_responses so the "M of N completed" headline renders either way.
| Flag | Description |
|---|---|
--submitted-only | Compute rollups against fully-submitted responses only |
--dev | Dev responses only |
--data-mode <m> | production, development, all |
