Docs / The CLI / Creating, moving, and deleting
Creating, moving, and deleting
Create, rename, delete, trash, restore, and template notes from the jotura CLI, including daily notes and the four template variables.
This page covers the commands that change which notes exist and where they live: create, rename, delete, the trash family, templates, and today. Changing the contents of a note is covered separately in Editing notes.
What these commands print
Most commands here emit JSON whether or not you pass --json, because they all change the vault. That group is create, rename, delete, trash <path>, trash restore, trash purge, trash empty, templates create, templates delete, and today --append.
The read-only commands in this group print plain text by default and switch to JSON when you ask for it. That group is trash list, templates list, templates show, and today --read.
Two forms sit between the two groups. A bare jotura today prints just the path in text mode, and every --dry-run respects --json like a read-only command does. The whole output contract is in Exit codes and JSON output.
Creating a note
create takes a parent folder and a file name as two separate arguments.
jotura create inbox "meeting notes"
| Argument or flag | Default | What it does |
|---|---|---|
<PARENT> | required | Parent folder, relative to the vault root. An empty string or . means the vault root itself. |
<NAME> | required | The file name. |
--template <NAME> | none | Fill the new note from a named template. |
--dry-run | off | Work out the target path and create nothing. |
--diff | off | Include a pseudo-diff in the output. For a create this is the fixed text --- /dev/null, +++ b/<path>, (file created). |
The success output is a single JSON object:
{ "path": "inbox/meeting notes.md" }
Missing parent folders are created for you, so jotura create projects/2026/q3 kickoff works even when none of those folders exist yet. This matters because there is no folder-creation command in the CLI at all. Folders come into existence only as a side effect of create, rename, and trash restore, and they disappear when their last file is removed.
Creating a note inside a share you can only view exits 15.
The extension rule
The command appends .md when the name does not already end in a recognized extension. jotura create notes idea produces notes/idea.md, while jotura create notes data.csv keeps the .csv. Only the final dot segment is examined, and the comparison ignores case, so NOTES.MD counts as recognized and report.v2 does not. An unrecognized suffix stays part of the name, so report.v2 becomes report.v2.md.
The list is the same one the file tree and sync use. These are the recognized text extensions in full.
| Kind | Extensions |
|---|---|
| Prose and data | md, markdown, mdc, txt, rst, org, tex, json, yaml, yml, toml, csv, tsv, xml, ini, conf, env, log |
| Markup and styles | html, htm, css, scss, sass |
| Code and scripts | js, mjs, ts, tsx, jsx, py, rb, rs, go, java, kt, swift, c, cpp, h, hpp, sql, sh, bash, zsh, fish, ps1, bat |
Document and image extensions such as pdf, docx, and png are recognized as well, and they are covered in Documents from the CLI.
Name collisions get a numbered suffix
If the target path already exists, the CLI does not overwrite it and does not fail. It inserts a space and a number before the extension and keeps counting until the name is free. Creating inbox/note.md a second time produces inbox/note (2).md, then inbox/note (3).md. The path field in the output is the suffixed path that was actually created, so read it rather than assuming your requested name was used.
One caveat sits with --dry-run: it reports the unsuffixed target. When the file already exists, the dry run predicts a path the real run will not use.
Creating from a template
Pass --template with a name from jotura templates list.
jotura create meetings "sprint review" --template meeting
The whole template file is copied, frontmatter included, and the template variables are substituted throughout. Substitution runs over the YAML block as well as the body, so a template whose frontmatter contains title: {{title}} gets a real title in the new note. A template name that does not exist exits 3. When a template is used, the output object carries a template field alongside path.
Renaming and moving
rename handles both renaming in place and moving between folders, because the destination is a full vault-relative path.
jotura rename inbox/idea.md projects/roadmap.md
| Argument or flag | Default | What it does |
|---|---|---|
<OLD> | required | The existing note or folder. A path that does not exist exits 3. |
<NEW> | required | The destination path. |
--dry-run | off | Work out the destination and move nothing. |
--diff | off | Include the pseudo-diff --- a/<old>, +++ b/<new>, (file renamed). |
The output names both ends of the move:
{ "from": "inbox/idea.md", "to": "projects/roadmap.md" }
from echoes the source argument you passed, and to is the destination the CLI actually used. Read to rather than assuming, because the extension rule below can change it.
For files, that rule mirrors create. If the new path already ends in a recognized extension it is used as given. Otherwise the source file’s extension is appended, falling back to .md. So jotura rename a.md b produces b.md and jotura rename t.txt t2 produces t2.txt. Folder renames are exempt, so a folder named 2026 stays 2026.
Missing destination folders are created for you. A destination that already exists is refused with exit 9 and the message already exists: <path>, so a rename never silently replaces another note. Renaming a document such as a PDF also removes its converted-text mirror under .md_store/, which the desktop app regenerates at the new path.
Note that rename --dry-run reads the source first, so a missing source exits 3 in a dry run exactly as it does in a real one.
Renaming inside a shared vault
Renaming a share mount, or a folder that contains one, rewrites the share registry the same way the desktop app does. A rename that crosses a mount boundary enqueues a delete in the source unit and a put in the destination unit, so the note moves for everyone rather than being duplicated. Sync and sharing are part of the paid plan, described on the pricing page.
Renaming does not rewrite links
This is the important limitation. rename moves the file and nothing else. Links in other notes are plain text in those notes, and the CLI does not touch them.
Whether a link survives depends on how it was written. Jotura resolves a link target by exact path first, with or without the .md. If the target contains no slash, it falls back to matching any note whose file name, minus the extension, is the same. A wikilink written as [[roadmap]] therefore keeps working when you move roadmap.md between folders, because the file name did not change. A link written as [[projects/roadmap]] or a markdown link to projects/roadmap.md breaks the moment the path changes. Renaming the file itself breaks both forms.
Check before you move something that other notes point at:
jotura backlinks projects/roadmap.md --json
backlinks and links are documented in Reading and searching. Fix the referring notes afterwards with jotura edit --replace, described in Editing notes.
Deleting permanently
delete unlinks the file or folder. There is no confirmation prompt.
jotura delete inbox/scratch.md
| Argument or flag | Default | What it does |
|---|---|---|
<PATH> | required | The note or folder to remove. Folders are removed recursively, including everything inside them. |
--trash | off | Soft-delete into the vault trash instead, exactly like jotura trash <path>. |
--dry-run | off | Work out the effect and delete nothing. |
--diff | off | Include a pseudo-diff. Without --trash this is --- a/<path>, +++ /dev/null, (file deleted). With --trash it is --- a/<path>, +++ b/<path>, (trash), naming the same path on both sides. |
The output is { "path": "...", "deleted": true }, or { "path": "...", "trashed": true } with --trash. Deleting a document also removes its .md_store/ mirror.
A missing path behaves differently in each form
Permanent delete treats a path that does not exist as a silent success and returns 0. A delete loop therefore cannot tell you that a file was already gone. The trash forms are stricter: both jotura delete missing.md --trash and jotura trash missing.md exit 3, because the trash reads the file’s metadata before moving it.
| Command | Path does not exist |
|---|---|
jotura delete missing.md | exit 0, nothing happens |
jotura delete missing.md --trash | exit 3 |
jotura trash missing.md | exit 3 |
jotura delete missing.md --dry-run | exit 3 |
jotura delete missing.md --trash --dry-run | exit 0, prints a manifest for a file that is not there |
Share guards
Deleting or trashing a share mount root, or a folder that contains one, is refused with exit 9. Leaving or unsharing a share happens in the desktop app, described in Sync and sharing.
Dry runs on create, rename, and delete
These three commands print a manifest envelope rather than the content envelope used by the editing commands. There is no body diff to show, so the envelope describes the structural change instead.
| Command | Dry-run JSON |
|---|---|
create | { "dryRun": true, "path", "kind": "create", "parent", "name", "template"?, "diff"? } |
rename | { "dryRun": true, "path": "<old>", "kind": "rename", "from", "to", "diff"? } |
delete | { "dryRun": true, "path", "kind": "delete", "permanent": true, "diff"? } |
delete --trash | { "dryRun": true, "path", "kind": "trash", "trash": true, "diff"? } |
Without --json, the same information prints as DRY RUN: <kind> <path> followed by one indented key: value line per extra field.
Two asymmetries are worth remembering when you script against these. create --dry-run does not model the collision suffix, and delete --trash --dry-run does not read the target at all, so it succeeds for a path that does not exist.
The trash
Soft-deleted items move to <vault>/.jotura/trash/, keeping their original folder structure, with a small .trashmeta file recording the original path, size, and the time of deletion. Because the folder name starts with a dot, trashed content stays out of the file tree, the search index, sync, grep, and backlink results.
The trash keeps every copy. Trash the same path twice and the second stored file gains a -<trashedAt> suffix, so both survive with their own metadata. That is why restore has to pick one and purge has to remove several.
trash accepts either a path or a subcommand, never both. Passing neither exits 9 with trash takes either a <path> or a subcommand (list|restore|empty|purge).
| Command | What it does |
|---|---|
jotura trash <path> | Move one note or folder into the trash. Prints { "path", "trashed": true }. |
jotura trash list | List everything currently in the trash. |
jotura trash restore <path> | Put a trashed item back. |
jotura trash purge <path> | Permanently delete the trashed copies of one original path. |
jotura trash empty --force | Permanently delete everything in the trash. |
Listing what is in there
jotura trash list --json
Items come back most recently trashed first, with ties broken alphabetically by original path. The JSON is an array of objects with originalPath, isFolder, size, and trashedAt, where trashedAt is milliseconds since the Unix epoch. In text mode each line is the original path, a tab, and that timestamp.
size is recorded only for files. A trashed folder always reports size: 0, whatever it contains, so summing the field will not tell you how much space emptying the trash would reclaim.
Restoring
The lookup key is the item’s original path, not its path inside the trash directory.
jotura trash restore inbox/scratch.md
jotura trash restore inbox/scratch.md --rename-to inbox/scratch-recovered.md
| Argument or flag | Default | What it does |
|---|---|---|
<PATH> | required | The original vault-relative path of the trashed item. |
--rename-to <PATH> | original path | Restore to a different path instead. |
If the same path was trashed more than once, the most recent copy is the one restored. If something already occupies the destination, the restore is refused with exit 9 rather than overwriting it, which is when --rename-to earns its place. Restoring into a share you only have view access to exits 15, and the guard is applied to whichever destination you end up with.
The output field name is a small trap. restoredAt holds the path the item was restored to, not a timestamp:
{ "originalPath": "inbox/scratch.md", "restoredAt": "inbox/scratch-recovered.md" }
Purging and emptying
jotura trash purge <path> permanently removes every trashed copy recorded under that original path, and prints { "originalPath": "...", "purged": true }. A path with nothing in the trash exits 3. Unlike trash <path>, trash restore, and delete, trash purge does not run the share guards.
jotura trash empty removes everything and requires --force. Without the flag it exits 9 with trash empty requires --force to confirm permanent deletion. The flag exists precisely because there is no undo.
jotura trash empty --force
The output is { "purged": 4 }, the count of items removed.
Trash versus permanent delete, and sync
Use trash or delete --trash when you might want the note back. Use delete when you are sure.
Neither is recoverable by anyone else. Both forms enqueue a delete for the share, so other members see the note disappear either way. The only difference is local: trashing keeps a copy in your own vault trash, which is never synced and never shared.
Doing several of these at once
jotura batch accepts a JSON array of operations that includes create, rename, and delete alongside the content operations. It is the right tool when a reorganization has to be planned as a unit.
Be precise about what “atomic” means there. Preconditions are checked for the whole batch before anything is written, so a hash conflict or a share violation aborts it with nothing changed. Once the apply phase begins, though, a failure part way through does not roll back the operations that already committed. A failing entry surfaces as exit 11 carrying opIndex, not as that entry’s own exit code. The input shape and the full phase model are in Editing notes.
Templates
Templates live at <vault>/.jotura/templates/<name>.md. Nested names such as work/standup are allowed and become nested folders. Because the templates folder is dot-prefixed, templates do not appear in the file tree or search results, are not indexed, and are not synced to other devices.
| Command | What it does |
|---|---|
jotura templates list | Print the available template names, sorted. |
jotura templates show <name> | Print a template. |
jotura templates create <name> | Create a template from stdin or from an existing note. |
jotura templates delete <name> | Delete a template. |
templates list respects --json and returns an object rather than a bare array:
jotura templates list --json
{ "templates": ["meeting", "work/standup"] }
templates show <name> prints the file contents in text mode, adding a trailing newline if the file lacks one. With --json it returns name, path, frontmatter, and body as separate fields, using the same frontmatter split described in Frontmatter and tags. A name that does not exist exits 3.
Creating a template
jotura templates create meeting --from "meetings/2026-08-12 standup.md"
| Argument or flag | Default | What it does |
|---|---|---|
<NAME> | required | The template name. meeting and meeting.md mean the same thing. |
--from <PATH> | stdin | Seed the template from an existing note, copying its full content including frontmatter. |
Without --from, the body is read from standard input until end of file:
printf '# {{title}}\n\nDate: {{date}}\n' | jotura templates create daily-log
Either way the output confirms where the template landed, which is the useful half when the name was nested:
{ "name": "meeting", "path": ".jotura/templates/meeting.md" }
Creating a template whose name is already taken exits 9 with template <name> already exists; delete it first or use a different name. There is no overwrite flag, so replacing a template means deleting it first.
jotura templates delete meeting prints { "name": "meeting", "deleted": true }. Deleting a template that does not exist exits 3.
The four template variables
Variables are substituted when a template is applied through create --template or today --template. Substitution is plain text replacement of every occurrence, with no escaping mechanism and no way to output a literal {{date}}.
| Variable | Expands to | Example |
|---|---|---|
{{date}} | The current date in UTC, as YYYY-MM-DD | 2026-08-30 |
{{datetime}} | The current time in UTC, as YYYY-MM-DDTHH:MM:SSZ | 2026-08-30T09:14:02Z |
{{filename}} | The new note’s file name without the folders or the .md | meeting-notes |
{{title}} | The file name title-cased, with hyphens and underscores turned into spaces | Meeting Notes |
There are exactly four. There is no {{time}}, no custom variable, and no conditional logic. Templates in the desktop app work the same way and are covered in Daily notes, tasks, and templates.
Daily notes with today
today resolves the path of a daily note, creating the note if it does not exist yet, and can read from it or append to it in one step.
jotura today
jotura today --append "- Shipped the release build"
jotura today --date 2026-08-12 --read
| Flag | Default | What it does |
|---|---|---|
--read | off | Print the note’s body instead of its path. |
--append [TEXT] | none | Append text to the note. With no value, the text is read from standard input. |
--date <YYYY-MM-DD> | today, in UTC | Operate on a specific date. |
--folder <FOLDER> | see below | The daily-notes folder. |
--template <NAME> | none | Fill newly created daily notes from a named template. |
The path is built as <folder>/<YYYY>/<MM>/<YYYY-MM-DD>.md, so a note for 3 May 2026 in a folder named Daily is Daily/2026/05/2026-05-03.md. In text mode the bare command prints just that path, which makes it easy to feed into another command.
An invalid --date exits 9. The validation is strict about both shape and calendar: 2026-1-05, 2026-13-01, and 2025-02-30 are all rejected.
The folder default reuses whatever case you already have
The help text says the default is daily, which understates what happens. Before falling back to that literal name, the CLI scans the vault root for any folder whose name matches daily ignoring case. An exact lowercase daily wins if it exists. Otherwise the first matching variant in sorted order is used. Only when no variant exists at all does the CLI create daily.
The desktop app is the reason this exists. Its daily_notes_folder setting defaults to Daily with a capital D, so a vault created there already has that spelling. On Windows and macOS the two spellings alias the same directory locally, but sync stores the path string, so Daily and daily would split into two folders on a Linux machine. Passing --folder explicitly bypasses the scan and uses exactly the name you gave.
Reading, appending, and their edges
--read takes precedence over --append. If you pass both, the note is read and nothing is appended. In text mode --read prints the file contents, adding a trailing newline if the file lacks one. An empty note prints nothing at all, which is what a freshly created daily note will do.
A bare --append with no value is the signal to read from standard input, which means you cannot append a genuinely empty string:
git log --oneline -5 | jotura today --append
Appending normalizes line endings around the block. A newline is inserted before your text if the file does not already end with one, and after it if your text does not end with one.
The append is written with a compare-and-swap against the hash read a moment earlier. If the desktop app changed the same daily note in between, the command exits 2 and you should retry from a fresh read. If the note was deleted or moved in between, it exits 14 instead and is deliberately not recreated.
--template applies only at creation. An existing daily note is never re-templated, so adding a template today does not retrofit yesterday’s note. Variables are substituted against the note’s file name, which means {{title}} in a daily note expands from the date stem, such as 2026 08 30.
One difference to keep in mind: jotura today uses the UTC date, while the desktop app uses your local date. Close to midnight the two can disagree about which note is today’s, and the window widens the further you are from UTC.
Output shapes
| Form | JSON fields |
|---|---|
Bare, --date, --folder | path, hash, created |
--read | path, hash, body, frontmatter, created |
--append | path, hash, created, appended |
The --append form prints that object whether or not you pass --json. The other two respect the flag: in text mode the bare form prints the path, and --read prints the file contents.
Exit codes you will see here
| Code | When |
|---|---|
| 0 | Success. Also a permanent delete of a path that does not exist. |
| 1 | An unclassified failure, such as an I/O error with no more specific mapping. Read the message field. |
| 2 | The compare-and-swap behind today --append lost to a concurrent write. |
| 3 | Source note, trashed item, or template not found. Also a trash or delete --trash of a path that does not exist. |
| 6 | No vault could be resolved. See Install and setup. |
| 8 | A permission error, or a path that resolves outside the vault. |
| 9 | A rename or restore whose destination exists, a templates create name that is taken, trash empty without --force, an invalid --date, an empty or traversing path, or a delete that would remove a share mount. |
| 11 | One entry in a jotura batch failed. The error carries opIndex and the underlying code. |
| 14 | A hash-checked write, including today --append, found its target deleted or moved since the read. |
| 15 | The target is inside a share you can only view. |
Exit code 2 is worth one extra check. Clap’s own usage errors, such as an unknown flag or a missing argument, also exit 2, but they print human-readable text on stderr rather than the JSON error envelope. Confirm that stderr parses as JSON before concluding you hit a hash conflict.
Errors are otherwise always a single-line JSON object on stderr with a stable code field, whether or not you passed --json. The complete table is in Exit codes and JSON output.
Paths are confined to the vault
Every path argument on every command here is normalized and checked. Backslashes become forward slashes, and repeated or empty segments collapse. A path that normalizes to nothing is rejected with exit 9 and the message path is empty.
A path containing . or .. components is rejected with exit 9, as is a path targeting the .jotura folder. The templates subtree is the one deliberate exception, which is how template names work at all. Symbolic links and Windows junctions that resolve inside the vault are followed, and anything resolving outside it exits 8 with path resolves outside the vault. The desktop app enforces the same rule through the same shared code.