Documentation source
Quick Capture
AI-powered natural language input that parses free text into structured entities with automatic type detection, field extraction, and suggested relations.
# Quick Capture
Quick Capture lets users type natural language into a single input bar and have it parsed by AI into a fully structured entity record. The system detects the appropriate entity type, generates a title, populates content fields, and optionally suggests relations to existing entities based on the current page context.
## Overview
Quick Capture solves the friction of structured data entry. Instead of navigating to a creation form, selecting a type, and filling in fields one by one, users type something like "New opportunity: automate invoice processing, estimated savings $200K/year, medium effort" and the system handles the rest.
The feature lives in `features/capture/` and is available in the sidebar via the `CaptureBar` component. It is page-context-aware: if a user is viewing the `opportunity` entity type list, the parser biases toward creating an opportunity. If viewing a specific entity detail page, the parser may suggest a relation to that entity.
## Key Concepts
**ParseResult** -- The Zod-validated output schema from the AI parser. Contains:
- `entityTypeSlug` -- which entity type to create
- `title` -- a generated title for the new record
- `content` -- field values matching the entity type's JSON schema
- `suggestedRelations` -- optional links to existing entities (target ID + relationship type)
**Page Context** -- The `usePageContext()` hook reads the current URL pathname to determine whether the user is on an entity type list page or an entity detail page. Reserved routes (dashboard, chat, feed, etc.) return empty context.
**Two-Step Flow** -- Capture uses a confirm-before-create pattern. The AI parses the input and shows a preview card. The user can edit the title, review extracted fields, then confirm or discard.
## How It Works
### 1. User Input
The `CaptureBar` component renders a compact input field in the sidebar; the floating `QuickCaptureBubble` (FAB) is available everywhere else. Three keyboard shortcuts open the popover:
| Shortcut | Behavior |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Cmd/Ctrl+.` | Toggle Quick Capture from anywhere — including inside text inputs. |
| `c` | Toggle Quick Capture when no typing surface is focused. The guard skips `<input>`, `<textarea>`, `<select>`, `contenteditable`, ARIA `textbox`/`combobox`/`searchbox`, and any cmdk command palette scope — so the shortcut never swallows a keystroke meant for a form field or the chat composer. |
| `a` | Same as `c`. Two single-key bindings give muscle-memory parity with the FAB position. |
The user types a free-text description and presses Enter.
### 1.5. URL-only fast path
When the trimmed input is exactly one URL (matched against `URL_ONLY_REGEX = /^https?:\/\/\S+$/i` and validated by `tryParseUrl` to reject malformed inputs and non-http(s) protocols), `parseCaptureInner` short-circuits the AI parser and routes deterministically:
1. Pick a tenant-resolved fallback type via `pickFallbackEntityType`. Preference order: `source-item` (the platform's canonical "ingested URL" type, wired to the `ingestUrl` agent) → `note` → any type with no required fields → first available type.
2. Derive a hostname-based title via `buildUrlOnlyTitle`. Strips `www.`, decodes percent-encoded segments (i18n-safe), strips file extensions, and appends a query distinguisher (`watch?v=...` → `youtube.com — watch (v=dQw4w9WgXcQ)`) so different videos don't collapse to identical titles while waiting for `ingestUrl` to overwrite with the real `<title>`.
3. Set `sourceUrl` on the `ParseResult` so the column-level `entities.source_url` is stamped, and so `applyRequiredFieldFallbacks` can populate URL-shaped required content fields.
The fast path NEVER calls the model — `safeGenerateObject` and `recordAIUsage` are skipped. Bypass is also skipped when the user has pinned an entity type via context (UI dropdown) — they explicitly chose where the URL should land.
### 2. AI Parsing
The `parseCapture()` server action:
1. Validates input with Zod — text must be 1 to `CAPTURE_MAX_LENGTH` characters (10,000; generous enough to cover in-flow dictation or a pasted email/meeting-note dump). On failure, throws a typed `CaptureInputError` with a user-facing message and logs the `ZodError` to Sentry at **warning** level via `captureNonFatal` (tagged `capture-parse:input-validation`). The clients mirror the cap via `maxLength={CAPTURE_MAX_LENGTH}` so typing stops at the limit before the server is hit; raw text is never used as the entity title — the AI generates the title separately via `ParseResultSchema.title`.
2. Fetches all available entity types with their JSON schemas
3. Builds a prompt that includes type descriptions and page context
4. Calls `safeGenerateObject()` (wrapping `generateObject()`) with the `ParseResultSchema` to produce structured output — failures on the AI output side surface as a grouped Sentry warning plus a clean `StructuredGenerationError`, not an unhandled `ZodError`.
5. Returns the `ParseResult` to the client
The AI receives the full set of entity types and their schemas, so it can pick the most appropriate type and populate fields accurately.
### 3. Preview and Confirm
The `CapturePreview` component renders a card showing:
- Entity type as a badge
- Editable title
- Extracted content fields as key-value pairs
- Suggested relations as link badges
The user can edit the title inline, then click Confirm or Discard.
### 4. Entity Creation
The `createFromCapture()` server action:
1. Validates the `ParseResult` with Zod
2. Filters content to schema fields and runs `applyRequiredFieldFallbacks` so every required field gets a sensible default (see "Required-field fallback strategy" below). The fallback layer is the load-bearing guarantee that creation never fails on `Missing required content fields`.
3. Creates the entity via the standard `createEntity()` action, passing `source_url` (when set by the URL-only fast path) so the URL lands on `entities.source_url` independent of the type's content schema.
4. Tags the entity with `metadata.created_via: "quick_capture"` for traceability — provenance markers live on `metadata`, NOT `content`, because content keys are validated against the entity type's `json_schema.properties` and any unknown key throws `EntityValidationError`. Mirrors the convention in `features/entities/server/resolve-relation-targets.ts` (`metadata: { created_via: "relation_inline" }`).
5. Creates any suggested relations in `entity_relations` (using `Promise.allSettled` so relation failures do not block entity creation).
6. Best-effort dispatches `routeCaptureUrlToSourceItem` — for any URL in the captured body, fires an Inngest event so a tenant agent wired with `ingestUrl` can ingest the page content asynchronously.
### Required-field fallback strategy
`applyRequiredFieldFallbacks` (in `features/entities/server/content-fallbacks.ts`) is the single source of truth for "what to put in a required content field the caller didn't fill." Every create path runs through it. Per-field strategy, most-specific to last-resort:
1. Already set → leave unchanged.
2. Schema declares `default` → use it.
3. Schema is `enum` with values → first enum value.
4. Schema is non-string primitive (`boolean` / `number` / `integer` / `array` / `object`) → typed zero value (`false` / `0` / `[]` / `{}`).
5. Schema is string with a URL-shaped field NAME (`url`, `source_url`, `link`, `href`, `*_url`) AND caller passed a `sourceUrl` hint → use it.
6. Schema is string → use the supplied `fallbackText` (typically the entity title), or `""` if `fallbackText` is empty.
Empty string is intentionally an acceptable last resort. An entity that exists with one empty required string is recoverable — the user can edit the form. A 500 toast on submit is not.
The AI parser prompt is explicit about this: "Required fields you can't determine WILL BE FILLED automatically with sensible defaults — DO NOT invent values to satisfy requireds, leave them omitted." This eliminates the failure mode where the model hallucinated a value to fit a required slot and tripped enum/format validation.
## API Reference
### Server Actions
| Function | Location | Purpose |
|---|---|---|
| `parseCapture(text, context)` | `features/capture/server/parse.ts` | AI-parse free text into `ParseResult` |
| `createFromCapture(input)` | `features/capture/server/create-from-capture.ts` | Create entity + relations from parsed result |
### Schemas
| Schema | Location | Purpose |
|---|---|---|
| `ParseResultSchema` | `features/capture/server/parse-schema.ts` | Zod schema for AI-generated structured output |
| `CAPTURE_MAX_LENGTH` | `features/capture/server/parse-schema.ts` | Shared char cap (10,000) — enforced client, server, and agent-handoff |
| `ParseCaptureInput` | `features/capture/server/parse.ts` | Input validation (text + optional context) |
| `CaptureInputError` | `features/capture/server/parse-schema.ts` | Typed error thrown on invalid input, carries user-facing message |
### Hooks
| Hook | Location | Purpose |
|---|---|---|
| `usePageContext()` | `features/capture/hooks/use-page-context.ts` | Extract entity type slug and entity ID from current URL |
### Components
| Component | Location | Purpose |
|---|---|---|
| `CaptureBar` | `features/capture/components/capture-bar.tsx` | Input field with keyboard shortcut, loading state, preview trigger |
| `CapturePreview` | `features/capture/components/capture-preview.tsx` | Confirm/edit/discard card for parsed results |
### Pure Functions
| Function | Location | Purpose |
|---|---|---|
| `getPageContextFromPathname(pathname)` | `features/capture/hooks/use-page-context.ts` | Extract context from a URL path (exported for testing) |
## For Agents
Agents do not interact with Quick Capture directly. Instead, they use the standard entity tools:
- **`createEntity`** -- create a record with structured content
- **`createRelation`** -- link two entities
- **`listEntityTypes`** -- discover available types and their schemas
Quick Capture is a human-facing convenience layer that ultimately calls the same `createEntity()` action that agents use.
## Related Modules
- **Entity System** (`features/entities/`) -- provides `createEntity()` and entity type schemas
- **Chat** (`features/chat/`) -- an alternative path for AI-assisted record creation via conversation
- **Navigation** (`features/navigation/`) -- CaptureBar is rendered in the sidebar layout