Convert Obsidian tasks between formats
Obsidian Tasks has no command that rewrites a vault from one format to another. Its documentation says so. The conversions below are scripts and recipes that do the job, each tested on the sample lines from the plugin's own docs, plus the one conversion where a tool exists: a task line to a task file and back.
Written against Obsidian Tasks 8.6, Task Genius 9.13 and TaskNotes 4.14, October 2026. Field names and symbols are listed on the cheat sheet.
Before you change anything
- Back up the vault. Copy the folder or commit it to git. A script that rewrites 4,000 lines should be easy to undo.
- Work on a copy first. Run the conversion on a copy of the vault, open it in Obsidian, and run one of your usual task queries. Compare the counts with the original.
- Convert, then switch the plugin. Obsidian Tasks reads one format at a time. Convert the lines first, then change the task format in its settings, so the plugin never sees a half-converted vault.
- Check what is left. Search the vault for the old symbols (📅, ⏳, ⏫ for emoji; due::, repeat:: for Dataview) to find lines the recipe skipped.
Which conversion do you need
| From | To | How |
|---|---|---|
| Emoji lines | Dataview fields | The script or find and replace recipe below. |
| Dataview fields | Emoji lines | The reverse script or recipe below. |
| Either | Task Genius | Nothing to rewrite. Task Genius reads both on the same line. Choose which one it writes in its settings. |
| Task lines | Task files | TaskNotes converts one line at a time. The TaskForge command line converts many at once. |
| Task files | Task lines | The TaskForge command line, with a preview of anything that would be lost. |
Emoji to Dataview fields
Each emoji and its value becomes a bracketed field. Priority emoji become a priority field with a word, and the recurrence text moves inside its brackets. Run the replacements in this order: dates and priority first, recurrence last, so the recurrence text ends where the next bracket begins.
Before and after
- [ ] Send invoice to Acme ⏫ 🔁 every month ➕ 2026-10-01 ⏳ 2026-11-01 📅 2026-11-03 #finance
- [x] Book flights 📅 2026-10-25 ✅ 2026-10-20
- [ ] Order parts 🆔 parts1
- [ ] Assemble 🛫 2026-11-10 ⛔ parts1 📅 2026-11-15 - [ ] Send invoice to Acme [priority:: high] [repeat:: every month] [created:: 2026-10-01] [scheduled:: 2026-11-01] [due:: 2026-11-03] #finance
- [x] Book flights [due:: 2026-10-25] [completion:: 2026-10-20]
- [ ] Order parts [id:: parts1]
- [ ] Assemble [start:: 2026-11-10] [dependsOn:: parts1] [due:: 2026-11-15] Find and replace, one field at a time
These patterns work in any editor with regex search across files, such as VS Code, which writes the captured text as $1. Turn on regex mode and match case. Unlike the script, they run on every line, not only on checkboxes.
| Field | Find | Replace with |
|---|---|---|
| 📅 | (?:📅|📆|🗓️?)\s*(\d{4}-\d{2}-\d{2}) | [due:: $1] |
| ⏳ | (?:⏳|⌛)\s*(\d{4}-\d{2}-\d{2}) | [scheduled:: $1] |
| 🛫 | 🛫\s*(\d{4}-\d{2}-\d{2}) | [start:: $1] |
| ➕ | ➕\s*(\d{4}-\d{2}-\d{2}) | [created:: $1] |
| ✅ | ✅\s*(\d{4}-\d{2}-\d{2}) | [completion:: $1] |
| ❌ | ❌\s*(\d{4}-\d{2}-\d{2}) | [cancelled:: $1] |
| 🔺 | 🔺️? | [priority:: highest] |
| ⏫ | ⏫️? | [priority:: high] |
| 🔼 | 🔼️? | [priority:: medium] |
| 🔽 | 🔽️? | [priority:: low] |
| ⏬ | ⏬️? | [priority:: lowest] |
| 🏁 | 🏁\s*(keep|delete) | [onCompletion:: $1] |
| 🆔 | 🆔\s*([A-Za-z0-9_-]+) | [id:: $1] |
| ⛔ | ⛔\s*([A-Za-z0-9_-]+(?:\s*,\s*[A-Za-z0-9_-]+)*) | [dependsOn:: $1] |
| 🔁 | 🔁\s*([A-Za-z0-9, !]*[A-Za-z0-9])(?=\s*(?:\[|\^|$)) | [repeat:: $1] |
The same thing as a script
Save this as emoji_to_dataview.py and run python3 emoji_to_dataview.py Inbox.md Projects/*.md. It touches only checkbox lines, writes two spaces before each field the way Obsidian Tasks does, and also converts the alternate due and scheduled symbols 📆, 🗓 and ⌛.
#!/usr/bin/env python3
"""Rewrite Obsidian Tasks emoji fields to Dataview [key:: value] fields.
Usage: emoji_to_dataview.py NOTE.md [NOTE.md ...] (rewrites in place; back up first)
Only checkbox lines are touched. Run it on a copy of your vault first.
"""
import os
import re
import sys
DATE = r"(\d{4}-\d{2}-\d{2})"
ID = r"[A-Za-z0-9_-]+"
RULES = [
(rf"(?:📅|📆|🗓️?)\s*{DATE}", r"[due:: \1]"),
(rf"(?:⏳|⌛)\s*{DATE}", r"[scheduled:: \1]"),
(rf"🛫\s*{DATE}", r"[start:: \1]"),
(rf"➕\s*{DATE}", r"[created:: \1]"),
(rf"✅\s*{DATE}", r"[completion:: \1]"),
(rf"❌\s*{DATE}", r"[cancelled:: \1]"),
(r"🔺️?", "[priority:: highest]"),
(r"⏫️?", "[priority:: high]"),
(r"🔼️?", "[priority:: medium]"),
(r"🔽️?", "[priority:: low]"),
(r"⏬️?", "[priority:: lowest]"),
(r"🏁\s*(keep|delete)", r"[onCompletion:: \1]"),
(rf"🆔\s*({ID})", r"[id:: \1]"),
(rf"⛔\s*({ID}(?:\s*,\s*{ID})*)", r"[dependsOn:: \1]"),
# Last, so the rule text ends at the next bracket or the end of the line.
(r"🔁\s*([A-Za-z0-9, !]*[A-Za-z0-9])(?=\s*(?:\[|\^|$))", r"[repeat:: \1]"),
]
TASK = re.compile(r"^(\s*(?:[-*+]|\d+\.)\s+\[.\]\s)(.*)$")
for path in sys.argv[1:]:
out = []
for raw in open(path, encoding="utf-8", newline="").read().split("\n"):
cr = "\r" if raw.endswith("\r") else ""
line = raw[: len(raw) - len(cr)]
m = TASK.match(line)
if m:
head, body = m.groups()
for pattern, repl in RULES:
body = re.sub(pattern, repl, body)
body = re.sub(r"\s*(\[(?:due|scheduled|start|created|completion|cancelled|priority|onCompletion|id|dependsOn|repeat):: )", r" \1", body)
line = head + body
out.append(line + cr)
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8", newline="") as fh:
fh.write("\n".join(out))
os.replace(tmp, path)
Dataview fields to emoji
The reverse reads both [key:: value] and (key:: value) and writes the standard emoji. Fields it does not know, like [project:: Website], stay on the line untouched, and a priority written as a number is left alone because the plugins disagree about what the numbers mean.
- [ ] Send invoice to Acme [priority:: high] [repeat:: every month] [created:: 2026-10-01] [scheduled:: 2026-11-01] [due:: 2026-11-03] #finance
- [x] Book flights [due:: 2026-10-25] [completion:: 2026-10-20]
- [ ] Order parts [id:: parts1]
- [ ] Assemble [start:: 2026-11-10] [dependsOn:: parts1] [due:: 2026-11-15] - [ ] Send invoice to Acme ⏫ 🔁 every month ➕ 2026-10-01 ⏳ 2026-11-01 📅 2026-11-03 #finance
- [x] Book flights 📅 2026-10-25 ✅ 2026-10-20
- [ ] Order parts 🆔 parts1
- [ ] Assemble 🛫 2026-11-10 ⛔ parts1 📅 2026-11-15 Run on the output of the previous script, it returns the sample lines above exactly. Real vaults are less tidy: lines that used 📆, 🗓 or ⌛ come back as 📅 or ⏳, and a line with an empty description gains a space.
Save this as dataview_to_emoji.py and run it the same way.
#!/usr/bin/env python3
"""Rewrite Dataview [key:: value] task fields to Obsidian Tasks emoji fields.
Usage: dataview_to_emoji.py NOTE.md [NOTE.md ...] (rewrites in place; back up first)
Reads [key:: v] and (key:: v). Only checkbox lines are touched.
"""
import os
import re
import sys
DATE = r"(\d{4}-\d{2}-\d{2})"
ID = r"[A-Za-z0-9_-]+"
def field(key, value):
return rf"[\[(]\s*{key}::\s*{value}\s*[\])]"
RULES = [
(field("due", DATE), r"📅 \1"),
(field("scheduled", DATE), r"⏳ \1"),
(field("start", DATE), r"🛫 \1"),
(field("created", DATE), r"➕ \1"),
(field("completion", DATE), r"✅ \1"),
(field("cancelled", DATE), r"❌ \1"),
(field("priority", "highest"), "🔺"),
(field("priority", "high"), "⏫"),
(field("priority", "medium"), "🔼"),
(field("priority", "low"), "🔽"),
(field("priority", "lowest"), "⏬"),
(field("onCompletion", "(keep|delete)"), r"🏁 \1"),
(field("id", f"({ID})"), r"🆔 \1"),
(field("dependsOn", rf"({ID}(?:\s*,\s*{ID})*)"), r"⛔ \1"),
(field("repeat", r"([A-Za-z0-9, !]*[A-Za-z0-9])"), r"🔁 \1"),
]
TASK = re.compile(r"^(\s*(?:[-*+]|\d+\.)\s+\[.\]\s)(.*)$")
for path in sys.argv[1:]:
out = []
for raw in open(path, encoding="utf-8", newline="").read().split("\n"):
cr = "\r" if raw.endswith("\r") else ""
line = raw[: len(raw) - len(cr)]
m = TASK.match(line)
if m:
head, body = m.groups()
for pattern, repl in RULES:
body = re.sub(pattern, repl, body, flags=re.I if "priority" in pattern else 0)
line = head + re.sub(r"[ \t]{2,}", " ", body).rstrip()
out.append(line + cr)
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8", newline="") as fh:
fh.write("\n".join(out))
os.replace(tmp, path)
What the conversion does not handle
- A line with text between two fields. Obsidian Tasks reads from the end of the line, so it will not have read those fields in the first place.
- A block link such as ^abc123 at the end of the line. The scripts leave it where it is, which is where Obsidian expects it.
- Task Genius only syntax: times after dates, [#A] priorities, project and context fields, iCalendar recurrence rules. Leave those lines alone or convert them by hand.
- Recurrence rules that one format spells differently. Check a few recurring tasks after the switch, especially ones with when done.
- Recurring tasks that already have completed copies in the file. The copies are ordinary done tasks and convert like any other line.
- Lines inside code blocks. The scripts work line by line and do not know about fences, so convert notes that quote examples by hand.
- An unrecognized field left at the end of the line, such as [custom:: x]. Obsidian Tasks reads from the end, so it stops there and ignores the converted fields before it.
- A 🔁 rule followed by a #tag. The recurrence is left unconverted, so move the tag before it or convert that line by hand.
- Tasks inside blockquotes or callouts (lines that start with > - [ ]). The scripts skip them.
- A time after a date, such as 14:30. It stays outside the converted field. Obsidian Tasks has no time field, so only Task Genius reads it.
Task lines to task files
A task file is a note with the task's fields in YAML frontmatter, the format TaskNotes uses. Not every line field has a place in it. This is how the fields map onto TaskNotes properties.
Where each field goes
| On a task line | In a task file | Note |
|---|---|---|
📅 2026-11-03 | | Same name. |
⏳ 2026-11-01 | | Same name. |
🛫 2026-11-10 | | TaskNotes has no start date. Keep it as a custom property if you need it. |
➕ 2026-10-01 | | Written as an ISO timestamp. |
✅ 2026-10-20 | | The status becomes a done value, and the date goes in completedDate. |
⏫ | | Uses the values in your TaskNotes priority list. The default list has three levels, not five, so decide how the five map. |
🔁 every month | | Becomes an iCalendar rule. every month on a due date of 2026-11-03 becomes DTSTART:20261103;FREQ=MONTHLY. |
#finance | | Become a YAML list, without the hash. |
🏁 delete | | No TaskNotes property. Kept as a plain frontmatter key that TaskNotes ignores. |
🆔 parts1 | | No TaskNotes property. Dependencies in TaskNotes point at file paths, not IDs. |
⛔ parts1 | | Becomes a blockedBy entry that names the other task's file. |
One line at a time with TaskNotes
TaskNotes has a Convert to TaskNote command, and a convert button next to checkboxes that you can switch on in its feature settings. Either one makes a new task file from the line and replaces the line with a link to it. Fine for a few tasks, slow for a vault.
Many at once with the TaskForge command line
TaskForge is a separate app, and its command line tool (also the engine behind its MCP server) converts tasks between a line and a file, in either direction, singly or in bulk. It needs a vault configured for task files, because it has to know how to recognize them. Preview first. A bulk commit must repeat the number of tasks the preview found, so a broad query cannot convert more than you meant.
taskforge convert --query 'tag:home' --to tasknotes --folder Tasks --dry-run
taskforge convert --query 'tag:home' --to tasknotes --folder Tasks --acknowledge-count 2 A real run
- [ ] Send invoice to Acme ⏫ 🔁 every month 📅 2026-11-03 #finance ---
title: Send invoice to Acme
status: open
priority: high
tags:
- finance
- task
recurrence: "DTSTART:20261103;FREQ=MONTHLY"
due: 2026-11-03
--- The ⏫ priority became high and every month became an iCalendar rule anchored on the due date. In a second run, a start date, an on-completion action and a task ID were kept as plain frontmatter keys (start, onCompletion, id) rather than dropped. The command refuses any task it cannot convert whole. Converting a file back to a line adds the file's identifying tag, task, to the line.
To go the other way, name a destination note, and preview first:
taskforge convert --query 'tag:home' --to inline --folder Inbox.md --dry-run Related
Questions people ask
How do I convert my Obsidian Tasks emoji to Dataview format?
Obsidian Tasks has no conversion command. Change the task format in its settings only after rewriting the lines, using regex find and replace or a script such as the one on this page. Back up first and test on a copy.
Will changing the task format in Obsidian Tasks convert my existing tasks?
No. The setting changes how the plugin reads and writes from then on. Lines in the old format stop being read as fields until you rewrite them.
How do I turn many checkbox tasks into TaskNotes files?
TaskNotes converts one line at a time. For a batch, the TaskForge command line converts every task matching a query, with a dry run first. Either way, check that fields without a TaskNotes property, such as start dates, ended up where you want them.
Do I need to convert at all if I use Task Genius?
Usually not. Task Genius reads emoji and bracket fields on the same line, so mixed vaults work as they are. Converting only helps if you also use Obsidian Tasks or Dataview, which read one style each.
Can the scripts damage my notes?
They change only checkbox lines and only the fields they recognize, but they rewrite files in place. Back up first, run on a copy, and compare before you trust them with the real vault.