bob capture¶
Capture tasks, bullets, and Pomodoro commands into the vault
bob capture --help
Capture one or more tasks or bullets into the Bob Obsidian vault.
TEXT is split into ordered capture items by one or more blank or whitespace-only physical lines; leading, trailing, and repeated separators are ignored. A whitespace-separated line of only Pomodoro session operators is also split into one item per operator, so `+2 x` means `+2`, blank line, `x`. A '@@<route>' or '@@<route>+<block-id>' declaration token anywhere in the draft is metadata, not a capture item: every otherwise-unrouted item inherits that destination, while an item-local @route, @route+id, @route#..., @route^..., @route:..., or trailing '#' marker still wins for that item. A declaration-only draft fails. Do not combine a textual @@ declaration with --route, --section, --task, or --task-section. Each item's first nonblank line is normalized and becomes that item's parent, formatted as a #task with a [created::] stamp unless a bullet or sub-bullet route is selected, and written to mac_inbox.md unless an @route token, @@ declaration, or --route target is provided. Existing target files prefer a Tasks section, then fall back to the last top-level task block. Missing target files are created when needed.
Within each item, every later physical line must be either a column-zero Markdown bullet or a nested bullet prefixed by exactly two ASCII spaces. At either depth, '-', '*', or '+' must be followed by a space or tab; the source marker and separator are stripped and the item is rendered with the canonical '- <body>' marker. Column-zero authored items render one target-selected indentation unit beneath the parent; two-space source items render two units beneath the parent and attach to the nearest preceding nonempty column-zero authored item. A marker-only placeholder is skipped and never clears that owner. Unsupported indentation, prose continuation, or an orphaned nested item fails with a usage error naming the physical line, and so does an item left with no text once its own capture markers are removed. Every recognized terminal 's:<N>', '$<N>', '%...', and '@route' marker configures only its own capture item and is stripped from the rendered line it was typed on; a second line in the same item resolving the same marker is ambiguous and fails before anything is written. Only an item's first line keeps the established leading '@route text' form. Authored children render before any clipboard children and the priority schedule log.
All items are planned against in-memory note snapshots before commit. Later items see earlier planned edits to the same target, and any parse, clipboard, validation, staging, or replace failure leaves notes, ledgers, and newly created clipboard files at their original state. Single-item JSON keeps its legacy shape; multi-item JSON keeps the first result at the top level and adds an ordered 'captures' array.
A draft beginning with `@@template:` expands one capture per later row. Write one capture item with `{0}`, `{1}`, and so on, then a blank line and one row of values per capture; each row must supply exactly one value per placeholder index; quote fields containing spaces. A template that uses only `{0}` takes one capture per value, so `bar baz bam` on one line is three captures. Capturing a template is exactly capturing its expansion, and the whole run still plans and writes atomically. Use `{{` and `}}` for literal braces, or the `--expand` option to print the expanded text without capturing. Templates refuse clipboard markers and Pomodoro session operators because those actions cannot safely repeat across rows.
Append a trailing lowercase 's:<N>' token, where N is a non-negative integer, to schedule the capture N days from today. The token is removed from the task text and rendered as [scheduled::YYYY-MM-DD] after [created::YYYY-MM-DD]. It may appear before or after a trailing @route token and is recognized only at the very end of the input. Checkbox-bearing captures with a resolved scheduled property start Blocked as '[?]', including s:0; ordinary bullet and sub-bullet forms stay checkbox-free.
Append a trailing '$' or '$<N>' token, where N selects the Nth priority level configured in ~/.config/bob/config.yml (1-4 today: P1-P4), to write [priority::<value>] and roll a random [scheduled::YYYY-MM-DD] inside that level's day window. A bare '$' means '$1'. A task with no priority field is implicitly P0, so there is no $0. An explicit s:<N> wins the scheduled date and $<N> still writes the priority. Like s:<N> it is recognized only in the terminal token region and may appear on either side of a trailing @route token. Single-quote '$<N>' in a shell; an unquoted or double-quoted $2 is expanded by the shell.
Append a trailing clipboard marker: '%' captures one live value without a header; '%<positive integer>' captures exactly that many values without headers, starting with the live clipboard and then recent history newest first; and '%<nonnumeric header>' captures one live value under an explicit header. For ordinary non-reference text, '%1' is equivalent to '%', while '%0' stays literal. Headers use letters, digits, '_' and '-'; '_' renders as a space. The marker composes with s:<N>, $<N>, and every route kind in either terminal order. Each value is classified separately: small text stays inline; 2-10 flat text lines and 1-10 flat unordered Markdown list items become child bullets, with source list markers removed; copied file paths are saved under img/ or file/; and long or other Markdown-structured text is preserved in a timestamped file/clip-*.md snippet. Clipboard children use the target note's dominant tab-or-two-space indentation and fall back to a tab. History captures fail without writing unless the exact requested count succeeds. For an eligible URL-only reference, for an eligible URL-only reference, exact standalone 'URL %' requests companion audio before clipboard parsing; the shipped 'URL @' spelling is still accepted. Use 'URL %1' or --clip URL to explicitly request clipboard capture for a URL. Live preview's --no-clip still recognizes eligible URL listen references, while ordinary clipboard markers remain literal. Use --clip[=HEADER] to force one live capture while keeping '%' tokens literal; --clip=<digits> requests a numeric header. Bare --clip also captures without a header. Use --no-clip to keep a genuine trailing '%...' token literal. Clipboard failures abort before the note or attachment files are changed.
Use '@<route>:<block-id>' in the same leading or trailing position to create a next-status task and link it from today's Pomodoro ledger. The routed task renders as '- [*] #task <body> [created::YYYY-MM-DD] ^<block-id>' when unscheduled, or '[?]' with any scheduled property before the final block ID. An optional '#<pomodoro>' slug, as in '@<route>:<block-id>#<pomodoro>', first targets a named open Pomodoro instead of the implicit current-or-future choice; whole-slug matches beat earlier prefix matches, and a typed '#' with an empty name is incomplete. If no open name matches, capture creates a canonical named future entry as '- [ ] () — NAME' and puts the task link beneath it. The daily note comes from BOB_DAY_FILE or <bob-dir>/YYYY/YYYYMMDD.md. Unnamed capture prefers the single open timed entry in its Pomodoros section and otherwise uses the first open entry. Named creation inserts after the current Pomodoro's complete block, else after the last completed Pomodoro's complete block, else before the first Pomodoro. Existing named matches skip the multiple-open-timed guard; named creation uses that guard because it needs an unambiguous current anchor. Both notes are fully validated before either is replaced; duplicate block IDs, duplicate Pomodoro links, missing ledger structure, no eligible unnamed target, invalid Pomodoro names, and multiple open timed entries for implicit or named creation fail without a partial capture. Append '=<X>' to start the session atomically, as in '@<route>:<block-id>=3' or '@<route>:<block-id>#<pomodoro>=-2': '<X>' mirrors the 'se<X>' snippet (empty, digits, '-', '-digits', or 'digits-' with optional digits; empty duration means 25 minutes, bare '-' means 5-minute offset, no '-' means zero offset). The start replaces the selected untimed '()' placeholder with '(**HHMM-HHMM** [t:: Nm])' computed from BOB_NOW at 5-minute rounding, then links the new task beneath it; any open timed entry fails with 'finish the current Pomodoro first'. The started entry moves ahead of every open Pomodoro, right after the last completed one. For '=', the first open placeholder wins, else an unnamed entry is created; for '#name=', an open name match wins, else a canonical named entry is created. '=<X>' cannot combine with 's:<N>' or '$<N>'.
Capture a whole item '@<route>:<block-id>[#<pomodoro>][=<X>]' to link an existing task into today's Pomodoro ledger instead of creating one, as in `bob capture '@sase:deep-fix'` or `bob capture '@sase:deep-fix#bugs=-'`. The marker must be the entire item: no body text, authored children, '%', 's:<N>', '$<N>', or forced destination flags. Ready and Blocked tasks become Next (a valid future schedule is retired with a pull-forward log entry); Next and In Progress tasks keep their status. With '#<pomodoro>' the link moves to that named entry (creating it when needed); without a name an already-queued task stays in its Pomodoro and '=<X>' starts that entry and moves it to the front of the queue, otherwise the implicit current/next entry is used. The '^' spelling ('bob capture '^sase:deep-fix='') executes identically and exists so typing '^' completes In Progress and Next tasks. JSON reports a distinct 'pomodoro_link' kind with the status transition, the ledger action (linked, moved, or already_current), and the resolved destination.
A single-token, single-line item starting with ':' (for example ':' or ':dee') is a task-picker query, never a capture: it fails with a teaching error so an unfinished query cannot create a junk inbox task. Accepting the picked task in a picker inserts its '@<route>:<block-id>' link, which captures exactly like a typed link.
Capture a whole item `!note:block-id` (for example `!sase:fix-flaky`) to complete that existing open task without closing a Pomodoro, the way `x!N` completes a linked task: the task line closes exactly as `x!N` writes it (including embedded subtasks), its Task Links retire the way `bob task reconcile` retires them, and Blocked dependents recover the way Ctrl+Enter recovers them. A fully unblocked dependent is offered; a successor Task Link is created only for a `--pull-in` pick (docs/task-dependencies.md §12; `plan.link_unblocked: false` turns linking off). The token must be the entire item: no body text, children, `%`, `s:`, `$`, `#name`, `=`, `@@`, or forced destination flags. The note locator works vault-wide exactly like `¬e:block-id` (`!projects/foo:bar`, `!"Shopping List":milk`). A bare `!` or partial token fails with a teaching error so an unfinished query cannot create a junk inbox task; accepting the picked task inserts its `!<note>:<block-id>` token. JSON reports kind `task_complete` with an additive `task_complete` object. Single-quote the argument: zsh history-expands `!`.
Append `¬e:block-id` to make the captured task depend on an existing task (`&foo:bar`; quote notes with spaces: `&"Shopping List":milk`). Task text creates a new owner. An existing-task marker with no text (`@note+id`, inherited `@@note+id`, bare `@note:id`, or bare `^note:id`) names that task as the owner and writes only the relationship. A `+` or `+query` token beside `&`/`*` opens the vault picker to choose the owner; submit refuses until a task is selected. Combine with `*note:block-id` in either order: `&` prerequisites of the owner land first.
Append `*note:block-id` to make the captured task a prerequisite of an existing open task (`*job:cleanup`; quote notes with spaces: `*"Shopping List":milk`). The named task gains a managed `DEPENDS ON` link to the captured task, flips to Blocked, and loses today's live Task Links; a new captured task is capped at Next. An existing owner selected with `@note+id`, inherited `@@note+id`, bare `^note:id`, or `--task`/`--route` may rise as far as In Progress. A Done or Canceled owner cannot block; reopen it first. Combine with `¬e:block-id` in either order: `&` prerequisites of the captured task land first.
Capture a whole item `+N` or `-N` (for example `+5` or `-2`) to adjust today's current timed Pomodoro by N five-minute units: `+5` extends by 25 minutes, `-2` shortens by 10 minutes. The count is optional and defaults to 1, so `+`, `-`, `++`, and `--` all work. The item must contain only the signed count (leading/trailing whitespace is fine); any extra text, marker, or child line fails instead of creating a task, and `Plan +5` stays ordinary prose. `+0`/`-0` fail, and oversized magnitudes fail checked arithmetic before any write. The adjustment selects the single open timed entry in today's `## Pomodoros` section, keeps the start fixed, recomputes `new_end = (start + new_duration) modulo 24 hours` with `new_duration = max(0, old + signed_units*5)`, and rewrites only that ledger line to Bob's canonical `(**HHMM-HHMM** [t:: Nm])` form, preserving other metadata, child bullets, and newline style. A `@@` declaration still routes ordinary items in the same draft but never turns an adjustment into a task. Forced destination, task, section, and clipboard options (`--route`, `--section`, `--task`, `--task-section`, `--clip`, `%`, `s:<N>`, `$<N>`) are rejected on adjustment items. Later items observe earlier staged edits to the same daily file, dry-run reports without writing, and any failure rolls the whole batch back. JSON reports a distinct `pomodoro_adjust` kind with an additive `pomodoro_adjust` object (direction, requested units/minutes, actual delta, before/after timing, line/name, rendered range); human output names what changed and where, and dry-run says what would change.
Capture a whole item `++N` or `--N` (for example `++3` or `--2`) to shift today's running timed Pomodoro N five-minute units later or earlier, keeping its duration: both endpoints translate modulo 24 hours like Obsidian's `N\o` / `N\O`. The count defaults to 1 (`++` / `--` move one unit); `++0` / `--0` and oversized magnitudes fail before any write, and the item must contain only the operator. JSON reports a distinct `pomodoro_shift` kind with an additive `pomodoro_shift` object; human output names what shifted and where, and dry-run says what would shift. The shell sees a leading `-` as a flag, so spell an earlier shift as `bob capture -- --2` (or `bob capture --1` for one unit); a bare `--` stays the end-of-options marker and carries no text.
Pomodoro sessions form a lifecycle: `=`/`=<X>` starts the next session, `=name` starts that named session, `+[N]`/`-[N]` resizes the running session, `++[N]`/`--[N]` shifts it, and `x` stops the running one (`=` starts the next session, `=name` starts that one, `x` stops the running one). Session operators may share one line when whitespace separates them: `+2 x` means `+2`, blank line, `x`, and `x =` closes then starts the next session. Quote `=` items in zsh, which expands a leading `=word` to a command path.
Batches that change today's `## Pomodoros` section also report the plan budget: a top-level `plan_budget` object with before/after theme and link meters, an `added_themes` list, and cap warnings that fire only while the batch grows a meter past its cap. Human output prints one `plan T/Tc themes · L/Lc links` meter line after the result plus one `bob capture: warning: …` stderr line per fired warning, and names where each new Task Link landed (`→ into running GOALS (0945-1015)`, `→ under GOALS (next up)`, `→ under GOALS (named)`, or `→ new Pomodoro BOB`; JSON reports the same destination with a `role` of `current`, `next_up`, `named`, or `created`). With `plan.strict: true` in the Bob config, a batch that creates a new named Pomodoro past the theme cap is refused atomically (exit 1, JSON `code: plan_theme_cap_exceeded`); session starts are never refused. An invalid plan config skips the budget with one plain warning. See `docs/capture.md` (`Plan budget and strict mode`) and `docs/plan.md`.
Capture a whole item `=`/`=<X>` (for example `=`, `=3`, `=-2`, `=2-1`) to start today's next future Pomodoro now with the same timing as the `se<X>` snippet: empty is 25 minutes, `3` is 15 minutes, `-` is 25 minutes with a 5-minute offset, `2-1` is 10 minutes with a 5-minute offset. Write `=name` (for example `=bugs`, `=3#bugs`, `=bugs@3`) to start the named Pomodoro now with `se<X>` timing: `=bugs` and `=#bugs` are one token; write `#` only when a number comes first (`=3#bugs`) or the name starts with a digit or reads as a close (`=#2fa`, `=#x2`). An open match (whole slug, else prefix) starts in place, a completed match starts a new session with that name (an "again" start), and otherwise a new named session is created and started. `<X>` goes before `#` or last after `@` (`=bugs@3` means `=3#bugs`); `=bugs=3` teaches `=bugs@3` or `=3#bugs`. Write a trailing `~<K>` drop list (for example `=~2`, `=3~2,4`, `=bugs~2`, `=3#bugs~1,3`) to start without those queued Task Links: `~` drops, so `=~2` drops task 2 from the session you start the way `x~2` drops task 2 from the session you stop, using the numbers the start lineup shows. The `~<K>` and `@<X>` parts come last, after the name when present, in either order (`=bugs~2@3` means `=3#bugs~2`). The item must contain only the start token (leading/trailing whitespace is fine) and have exactly one physical line; a claimed token (counted or drop-carrying) with extra text, markers, or child lines (`=3 more`, `=3x`, `=~2 more`) and an exact token with child lines fail instead of creating a task, while a bare token with prose (`= foo`, `= ~2`) and mid-body tokens (`Plan =3`, `Plan =bugs`) stay ordinary prose. A dangling `~`/`,` (`=~`, `=~2,`) is incomplete and fails. A bare `=`/`=<X>` start needs a future `- [ ] ()` placeholder; a named start creates its session when no open entry matches. With one timed entry running, `=` restarts it and `=<X>` restarts it with that timing. `=name` swaps a different named Pomodoro in and keeps the running clock; `=3#name` swaps it in with fresh timing. Naming the running session with no timing refuses and teaches `=` or `=<X>` to restart it. The started entry moves to the current slot and reports its queued Task Links, numbered 1..N in ledger order with a numbered human index column. A `@@` declaration never applies to start items, and forced destination/task/section/clipboard options are rejected on them. Later items see earlier staged edits, dry-run reports without writing, and any failure rolls the whole batch back, so `x`, blank line, `=` switches sessions atomically.
The retired `==` start family fails with one teaching error that names its single-`=` rewrite: for example, `==3#bugs` says to write `=3#bugs`. The `==x` family is not a close and teaches the bare `x` rewrite. A bare `==` followed by prose stays ordinary text, preserving Obsidian highlights. JSON stays `pomodoro_start`; `pomodoro_start.override` is present only for a restart or swap and its action is `restart` or `swap`. See `docs/capture.md` (`Overriding the running Pomodoro`).
Capture a whole item `x[<N>][*<P>][!<M>][~<K>][@<N>]` (lowercase `x` only, with `*`, `!`, `~`, and `@` in any order; `<N>` may instead be typed last as `@<N>` (for example `x!1@2` means `x2!1`)) to close today's running timed Pomodoro the way Obsidian's Ctrl+Enter completion does, plus an auto-decrement that shortens an early-stopped session to the earliest five-minute step at or after now (never extended; an overrun is reported). A bare `x` keeps today's behavior; `x<N>` keeps only the numbered Task Links in `<N>` in progress and defers the rest, `x*<P>` parks the links in `<P>` (normal In Progress work without carrying them forward), `x!<M>` completes the links in `<M>`, `x~<K>` drops the links in `<K>` (removed from the closed session, not carried and not started; a dropped task keeps its lane), and combined forms do each part at once; completing planned tasks also links what they unblocked into the closed session's continuation (docs/task-dependencies.md §12; `plan.link_unblocked: false` turns linking off). `<N>`, `<P>`, `<M>`, and `<K>` are comma-separated task numbers in ledger order starting at 1 (the numbers `bob capture` shows, in human output, in `--dry-run`, and as JSON `task_links`); a lone `0` means no task stays in progress, as in `x0`. Plain `x0` on a note-free session resets it to the first future placeholder (clearing the parenthesized ledger to `()` and keeping all children); the same `x0` on a session with a stand-alone note closes, deferring every numbered link. Explicit modifiers (`x0*2`, `x0!2`, `x0~2`, wildcards) always close. A present `*` or `!` group with no digits selects every numbered link left after explicit assignments (`x*` parks all, `x!` completes all, `x1*` keeps 1 in progress and parks the rest, `x*!2` parks the rest and completes 2). Explicit forms such as `x*1`/`x!1` select only task 1; an absent group stays absent and `~` never defaults. Two empty wildcard groups compete, so `x*!` and `x!*` need numbers on at least one group. Log one entry on the close line: `x wired the lexer` logs to the first task the close works (task 1 for a plain `x`, the first eligible worked link with wildcard intent), and `x1,3 3 fixed the flaky test` logs to task 3. A leading number names the task, so only the entry's first token is an index (`x 1 3 bugs fixed` logs `3 bugs fixed` to task 1). Write the entry right after the close, then any session operators (`bob capture 'x wired the lexer'`, `bob capture 'x1,3 3 fixed the flaky test'`, `bob capture 'x wired the lexer ='`). For several entries or details, use child bullets below the close instead: `- [<n>] <text>` logs `<text>` in typed order, and a two-space ` - <detail>` nests an undated detail under its entry in the task's Work Log. Bullets are numbered all or none: either every first-level bullet starts with its task number (`- 3 foo bar`), or none does and each bullet logs in order to the close's worked tasks — one worked task takes them all, otherwise bullet `i` logs to worked task `i`, so `x3,4` plus `- foo bar` plus `- baz bam` writes exactly what `- 3 foo bar` plus `- 4 baz bam` writes. A leading number is always a task number, so to log text that starts with a number, number every bullet (`- 3 2 bugs fixed`). For explicitly numbered selections without wildcards, positions resolve lexically and `capture-parse` agrees with `bob capture`. With wildcard intent, numbered entries can target resolved worked links except explicit drops; unnumbered bullets wait for the staged top-level worked lineup, and an unnumbered inline entry logs once to its first eligible link. Without wildcard intent, only the explicit numbers in `<N>`, `*<P>`, or `!<M>` take entries; otherwise every number starting at 1 takes one except `~<K>`. Only the first token is an index, so `- 1 fixed 3 bugs` logs `fixed 3 bugs` and every backslash stays literal. The outcome is exactly the sub-bullets the user would type by hand under that Task Link, followed by the unchanged close. A close's child lines are Work Log bullets; a token ending in `,` or `~` (`x1,`, `x*1,`, `x*~`) is incomplete and fails, a dangling bullet (`- 1`) or dangling inline number (`x 1`) is incomplete and fails, whitespace is never allowed inside the lists, and `xx`/`xa`/`Plan x`/a capitalized `X…` stay ordinary prose. A trailing `*`/`!` with no numbers is a valid wildcard group instead of a dangling state, so `x*` and `x!` are valid. Entry text is literal (`@route`, `@@route`, `s:<N>`, `$<N>`, `%`, `#`, and `:query` stay text; plain wikilinks are allowed), while block links, embeds, and code fences are rejected. An inline entry plus child bullets fails with the bullet to write. A chain line's bullets attach to its `x`, and an inline entry chains too (`x wired the lexer =` closes with the entry then starts, `-2 x wired the lexer` shortens then closes). `@route:block-id=x…` (and the `^` forms; the `=` separates the suffix from the block ID, and the retired `=*…`/`=!…` aliases fail with a teaching error) first put that existing task into the running session then close it, and `<text> @route:block-id=x…` creates the new task in the running session then closes it; explicit numbers and wildcard expansion refer to the post-link lineup, and `#name` with a close fails because only the running session can close. Later items see earlier staged edits, dry-run reports without writing, and any failure rolls the whole batch back, so `printf -- '-2\n\nx\n' | bob capture` adjusts then closes atomically. JSON reports a distinct `pomodoro_close` kind (link and task forms keep their kind with an additive `pomodoro_close` object) carrying the typed `raw`, the `in_progress`/`park`/`complete`/`drop` lists, the `log` entries (`index`, `text`, `details`) in typed order, the numbered `task_links` lineup (with `parked` and `dropped` outcomes), each task row's `index` and `role` (with a `dropped` role; parked rows stay `worked` with `carried: false`) plus its `typed_work_log` subset and the aligned `typed_work_log_details`; human output names the session, the range change, the file, and the line, prefixing numbered rows with their index, printing typed entries first uncapped and not dimmed, listing dropped rows (with a `stays <status>` lane caption), marking parked rows `Parked · not carried`, and summarizing `Parked <P>` and `Dropped <K>`. Single-quote the argument: zsh expands a leading `=word` and `!` history expansion applies.
Use '@<route>^<block-id>' in the same leading or trailing position to create an ordinary open task with the requested trailing Obsidian block ID, without creating or modifying a Pomodoro ledger link. It renders as '- [ ] #task <body> [created::YYYY-MM-DD] ^<block-id>' when unscheduled, or '[?]' with priority and scheduled properties before the final block ID. Duplicate block IDs in the destination note fail before the note is replaced; a missing destination note may still be created like any ordinary routed task. This form never reads or requires the daily note. The retired '@<route>::<block-id>' spelling is no longer accepted; use '@<route>^<block-id>' instead.
Append '+' immediately after the block ID, as in '@<route>^<block-id>+' with an optional '#<pomodoro>' name ('@<route>^<block-id>+#<pomodoro>'), to create a brand-new sub-project note '<route>_<block-id with every '-' replaced by '_'>.md' at the vault root instead of a task. The note carries a 'parent: "[[<route>]]"' link, a '[[project]]' type with 'wip' status, and a '- [ ] #task #prj <body> #hide ^prj' lifecycle task, mirroring the Obsidian 'Create project note from task' command. The '^prj' task is never linked and never starts as '[*]'. The route is lower-cased and the block ID keeps its authored case. The parent '<route>.md' must already exist as an area or non-terminal project note; the new note must not exist yet; the parent note itself is never modified ('bob projects sync' owns its Sub-projects line). Authored child bullets become '## Tasks' entries or '## Title Case' sections from ALL-CAPS titles in the new note. 's:<N>' writes inline '[scheduled:: YYYY-MM-DD]' on the '^prj' line and '$<N>' writes an inline priority there, while '%...' and --clip are rejected. The '#<pomodoro>' name picks the Pomodoro that ' :<id>' Task Links go under; with no such task the name is rejected as unused. The retired '@<route>:<block-id>+' spelling is no longer accepted; use '@<route>^<block-id>+' instead. End a first-level task bullet with ' :<block-id>' to name it, make it Next ('[*]', or '[?]' when the project is scheduled), and link '[[<stem>#^<block-id>]]' into the current/next Pomodoro or the named one; end it with ' ^<block-id>' to name it only. Named tasks render the ID last, after '[created::...]'. The project note and the daily note are written together, so a duplicate link or a missing daily note writes nothing.
Use '@<route>+<block-id>' in the same leading or trailing position to capture an ordinary child bullet beneath an existing task without a [created::] stamp. It renders as '- <body>' and writes only the optional scheduled property. The complete new child is placed before the selected task's first direct-child Schedule Log or Work Log; if neither managed log exists, the child is appended at the end of the task block. The note and task must already exist. Existing child indentation and line endings are preserved; run 'bob capture-tasks -r <route>' to list eligible task block IDs. A trailing '#<section>' component, as in '@<route>+<block-id>#<section>', names an ALL-CAPS child section of that task. The selector may use A-Z, a-z, 0-9, and & ' ( ) , . / -. Whole-slug matches beat earlier prefix matches, so #future-work still wins over FUTURE WORKFLOW. The captured block is appended at the end of that section, before a managed log nested under it, using the section's child indentation. A selector that matches nothing is an error listing the task's real sections; capture never falls back to the end of the task. '@<route>+<block-id>#' with an empty selector is incomplete and needs a task section; run 'bob capture-task-sections -r <route> -i <block-id>' to list them. A marker-only '@<route>+<block-id>' item with no body text ensures that task is Next and relocates its existing open-Pomodoro Task Link to today's implicit current/next Pomodoro without creating a missing link or toggling Next back to Ready. The same marker-only form with a Pomodoro selector, '@<route>+<block-id>#<pomodoro>', ensures Next and moves that existing Task Link subtree to the named open Pomodoro, or creates that named future Pomodoro and moves the subtree there. Neither unsuffixed form synthesizes a missing Task Link. A terminal '!' on the unsuffixed marker-only form, '@<route>+<block-id>!', is the only spelling that toggles the Pomodoro Task Link itself: when no matching link sits under an open Pomodoro the task links (Ready/Blocked rise to Next while Next/In Progress keep their lane), and when a matching link is already there every such link is removed with the lane unchanged; '!' cannot be combined with '#<pomodoro>'.
Append a bare trailing '#' to capture the item as a plain-text sub-bullet on a Pomodoro instead of a task. It renders as '- <body>' with no [created::] stamp, no '#task' marker, and no block ID. The daily note comes from BOB_DAY_FILE or <bob-dir>/YYYY/YYYYMMDD.md. Capture prefers the single open timed entry in its Pomodoros section, otherwise the last completed entry, and otherwise the first open entry, appending the new bullet at the end of that entry's child block. The marker composes with '%...' and --clip but is rejected alongside 's:<N>', '$<N>', '@route', and --route.
Append '#<section-prefix>' or a bare '#' to an @route token (such as '@notes#Ideas' or '@notes#') to capture an ordinary bullet instead. It renders as '- <body> [created::YYYY-MM-DD]' and is placed in a non-Tasks section whose heading title starts with the prefix (compared case insensitively), or any non-Tasks section for a bare '#'. The Next & In Progress, Blocked, and Done & Canceled status-group headings bob task reconcile generates under a Tasks heading belong to that Tasks section and are never bullet targets. A matching non-H1 section is preferred; a matching H1 heading is used only when no non-H1 heading matches. The marker may lead ('@notes#Ideas jot idea') or trail ('jot idea @notes#Ideas') the body. A standalone terminal '#<section-prefix>' marker (not appended to an @route token) is still not accepted and fails with a usage error; a standalone bare '#' is the Pomodoro-note marker described above, not a bullet marker.
Use --route with --section to force bullet mode while keeping @tokens literal. The section title is matched exactly, case insensitively, against non-Tasks headings (generated status-group headings under Tasks are skipped); if no heading matches, the bullet falls back to the pre-heading section.
Use --route with --task and --task-section TITLE to nest a sub-bullet under the named ALL-CAPS child section of that task. The title is matched exactly, case insensitively; unlike a typed #<section> selector, it is not slug- or prefix-matched, so --task-section future-work does not match FUTURE WORK.
Use expansion mode to print a template draft's expanded capture text and exit before vault access or planning. In expansion mode all other capture options are accepted and ignored.
Usage: bob capture [OPTIONS] [TEXT]...
Arguments:
[TEXT]...
Task text; multiple args are joined with spaces; unknown `--flag` before `--` is rejected
Options:
-b, --bob-dir <DIR>
Bob vault root; defaults to BOB_DIR or ~/bob
-c, --clip[=<HEADER>]
Capture the clipboard, optionally with HEADER
-d, --dry-run
Plan and report without writing notes or clipboard files
-E, --expand
Print the draft with its @@template: expanded, then exit without capturing
-f, --format <FORMAT>
Output format: human or json
[default: human]
[possible values: human, json]
-h, --help
Show help
-i, --inbox-route <ANSWER>
Route an inbox task this capture changes: <N|KEY>=<NOTE> moves it, <N|KEY>= keeps it in its inbox, keep keeps every otherwise unanswered candidate; repeatable
-n, --no-clip
Keep trailing %... clipboard markers literal
-R, --no-ref
Keep bare links as inbox tasks instead of queueing them for the reading queue
-p, --pull-in <PICKS>
Pull in successor offers: all, none, or a comma list of offer numbers and keys ($<N> in TEXT is still priority)
-r, --route <NAME>
Force the route to NAME.md and keep @tokens in text literal
-s, --section <TITLE>
Force a bullet into the exact section TITLE; requires --route
-t, --task <BLOCK-ID>
Append beneath task BLOCK-ID; requires --route
-S, --task-section <TITLE>
Nest under the exact task-section TITLE; requires --route and --task
Examples:
bob capture buy milk @groceries
bob capture buy milk s:1
bob capture buy milk s:2 @groceries
bob capture buy milk @groceries s:2
bob capture buy milk '$2'
bob capture research rust '$4' @dev
bob capture buy milk %
bob capture research links %3
bob capture investigate %log @dev:blockid
bob capture --clip=screenshot -- save dashboard
bob capture '@dev^foobar' 'Some ordinary task.'
bob capture '@dev:foobar' 'Some foobar task.'
bob capture '@dev:foobar#bugs' 'Some foobar task.'
bob capture '@cash^goog-exit+' 'Finish the Google exit packet!'
printf 'Finish the Google exit packet! @cash^goog-exit+\n- Draft the resignation memo\n' | bob capture
printf 'Finish the Google exit packet! @cash^goog-exit+#admin\n- Draft the resignation memo :draft-memo\n - keep it short\n- Collect the equity paperwork ^equity-docs\n' | bob capture
bob capture '@cash+goog-exit' 'Called Morgan Stanley today.'
bob capture '@cash+goog-exit!'
bob capture +5
bob capture -- -2
bob capture +
bob capture ++3
bob capture -- --2
bob capture --1
bob capture -- --anything
printf '+5\n\nCall bank @Cash+\n' | bob capture
printf -- '--2\n\n+\n' | bob capture
bob capture 'x'
bob capture 'x2'
bob capture 'x1!2'
bob capture 'x!1@2'
bob capture 'x1~2'
bob capture 'x0'
printf 'x2,3\n- 2 wired the lexer\n' | bob capture
printf 'x =\n- 1 wired the lexer\n' | bob capture
bob capture '='
bob capture '=3'
bob capture '=~2'
bob capture '=3#bugs~1'
bob capture '=bugs'
bob capture '=3#bugs'
bob capture '=bugs@3'
bob capture 'x =bugs'
bob capture '@sase:deep-fix=bugs'
bob capture 'x =~2'
printf 'x\n\n=\n' | bob capture
bob capture '+2 x'
bob capture 'x ='
bob capture '^bob:capture-stop=x'
bob capture '^bob:capture-stop=x!1'
bob capture -p 1 -- '!sase:fix'
bob capture -p all -- '!sase:fix'
bob capture -i 1=health -- '@mac_inbox:route-task'
printf -- '-2\n\nx\n' | bob capture
bob capture 'Postgres 17 minimum @foo+bar#requirements'
bob capture --route foo --task bar --task-section REQUIREMENTS -- 'Postgres 17 minimum'
bob capture remembered to bump the timeout #
bob capture paste the failing output % #
bob capture jot idea @notes#Ideas
bob capture --route notes --section Ideas -- jot idea
bob capture @notes#Ideas jot idea
printf '@@foo\nFirst task\n\nSecond task @bar\n' | bob capture
printf '@@template:\nApply to {0}! @job^apply-{1}\n\nFoobar foo\n"Two Sigma" two-sigma\n' | bob capture
printf '@@foo+a-id\nFirst note\n- authored detail\n\nSecond note\n' | bob capture
echo 'buy milk @groceries' | bob capture
bob capture -f json -- @work send status
printf 'Prepare launch\n- Confirm owner\n\nSend status @work\n' | bob capture
printf 'Prepare launch\n- Confirm owner\n- Attach checklist\n' | bob capture
bob capture https://example.com/essay
bob capture 'Ask Kelly *job:cleanup'
bob capture '*job:cleanup @body+work'
bob capture '&foo:bar @body+excercise'
bob capture '^body:excercise &foo:bar'
Environment:
BOB_CAPTURE_TEMPLATES_FILE exact saved-template shelf path; defaults to $XDG_STATE_HOME/bob-cli/capture/templates.json
BOB_CLIPBOARD_CMD whitespace-split command that prints the live clipboard; overrides platform tools
BOB_CLIPBOARD_HISTORY_CMD whitespace-split history command; receives count and prints a newest-first JSON array of strings
BOB_CONFIG_FILE exact bullet-property config file; defaults to $XDG_CONFIG_HOME/bob/config.yml or ~/.config/bob/config.yml
BOB_DAY_FILE exact daily note used by Pomodoro-linked capture
BOB_DIR Bob vault root when --bob-dir is omitted
BOB_NOW current date/time override
BOB_PRIORITY_ROLL_SEED fixed seed for $<N> rolls; unset means random
XDG_CONFIG_HOME base config directory for BOB_CONFIG_FILE's default; defaults to ~/.config
XDG_STATE_HOME base state directory for the saved-template shelf; defaults to ~/.local/state
Clipboard source order:
Live: BOB_CLIPBOARD_CMD; macOS pbpaste; Linux wl-paste or xclip/xsel; tmux show-buffer
History: BOB_CLIPBOARD_HISTORY_CMD; otherwise read-only Clipy SQLite on macOS; no automatic provider elsewhere