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 commands that put a file in them, such as create, rename, and trash restore. Removing a folder’s last file leaves the empty folder in place, and jotura delete <folder> removes it.
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, canvas, base |
| 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, after the empty note has already been created. 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 and reports the link updates described below:
{ "from": "inbox/idea.md", "to": "projects/roadmap.md", "linksUpdated": 2, "linksSkipped": 0, "linkErrors": [] }
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 moves its converted-text sibling note with it, so report.pdf.md follows report.pdf to its new name, and the rename is refused with exit 9 when the sibling’s new path is already taken.
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 rewrites links
After the move, rename updates the wikilinks and markdown links in other notes that pointed at the old path, using the same shared code as the desktop app. A link keeps its written shape where it can: a bare name stays a bare name and a partial path keeps its depth while that form still resolves to the renamed note, otherwise it becomes the full new path. Relative markdown links stay relative, and aliases, anchors and the presence or absence of .md survive. Links that reach the note through a frontmatter alias still resolve and are left alone. Renaming a folder rewrites only the path segments the rename changed.
Each rewrite is a hash-checked write. A note edited while the rename runs is planned again from its new content and tried once more. If it changed again it is skipped and counted in linksSkipped, so a concurrent edit is never overwritten. Notes inside a share you can only view are left as they are. A rewrite that fails never undoes the rename: it lands in linkErrors as an object with path and error. linksUpdated counts the notes that were rewritten.
A rename entry inside jotura batch moves the file only and does not rewrite links. To see what points at a note before you move it:
jotura backlinks projects/roadmap.md --json
backlinks and links are documented in Reading and searching.
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 leaves its converted-text sibling note, such as report.pdf.md, in place as an ordinary note.
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 and the existing file stays put. Pass --rename-to in that case. Restoring into a share you only have view access to exits 15, and the guard is applied to whichever destination you end up with.
Despite its name, the restoredAt field holds the path the item was restored to. It carries no 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 is required because emptying the trash cannot be undone.
jotura trash empty --force
The output is { "purged": 4 }, the count of items removed.
Trash versus permanent delete, and sync
Reach for trash or delete --trash when you might want the note back, and keep plain delete for files you are sure about.
For everyone else in a share, the two behave the same. Both forms enqueue a delete, so other members see the note disappear and have no copy to recover. Trashing only differs on your machine, where it keeps a copy in your own vault trash. That trash 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.
A vault that came from Obsidian may also name a templates folder in .obsidian/templates.json. The notes in that folder are listed and applied alongside Jotura’s own templates, but the CLI treats them as read-only: templates create and templates delete only ever write to .jotura/templates/. Edit an Obsidian template as an ordinary note.
| Command | What it does |
|---|---|
jotura templates list |
Print the available template names, sorted, from both folders. |
jotura templates show <name> |
Print a template from either folder. |
jotura templates create <name> |
Create a template in .jotura/templates/ from stdin or from an existing note. |
jotura templates delete <name> |
Delete a template from .jotura/templates/. |
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 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 |
{{time}} |
The current time in UTC, as HH:mm |
09:14 |
{{datetime}} |
The current time in UTC, as YYYY-MM-DDTHH:MM:SSZ |
2026-08-30T09:14:02Z |
{{date:FMT}} |
The current date in a custom format built from the tokens YYYY, YY, MM, M, DD, D, HH, H, mm, ss |
{{date:DD/MM/YYYY}} gives 30/08/2026 |
{{time:FMT}} |
The current time in the same custom format | {{time:HH:mm:ss}} gives 09:14:02 |
{{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 seven. The two formatted forms match Obsidian’s core Templates plugin, so an Obsidian template applies cleanly. There is no custom variable and no conditional logic, and Templater syntax is copied through as text. 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.
In a vault that has .obsidian/daily-notes.json, the layout comes from that file instead, exactly as it does in the desktop app: the folder key names the folder, the format key names the file using the tokens YYYY, YY, MM, M, DD, and D, and the template key seeds new notes. A format using any other token, or one that does not name a full date, falls back to Jotura’s own <folder>/YYYY/MM/YYYY-MM-DD.md layout in the folder Jotura would otherwise use. So does a vault whose Jotura daily folder already holds daily notes while the Obsidian folder holds none.
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. The CLI first uses the daily notes folder set for the vault in the desktop app, read from the dailyNotesFolder key of Jotura/settings.json. When that is not set, and before falling back to the literal name, it 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 an 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.