Overview of the Script
The LogSeq to Outlook Events Script automates the creation of Outlook calendar events from scheduled blocks in your LogSeq markdown files. In each run, it:
-
Loads configuration (graph name, LogSeq directory, look-ahead window) from
config.json
(or prompts if missing) . -
Scans all
.md
files under the specified LogSeq directory for list items (- β¦
) containing aSCHEDULED: <YYYY-MM-DD [HH:MM]>
tag and ablockID:: β¦
.
You need to create a property namedblockID
within the scheduled event and set its value to the block reference number without parentheses. -
Deduplicates against:
- Previously created events (tracked in
outlook_blockids.json
) - Existing Outlook events whose bodies contain a matching LogSeq blockID
- Previously created events (tracked in
-
Creates new Outlook appointments for any future scheduled items not yet synced:
- Sets a 60-minute duration with a 10-minute reminder
- Writes the LogSeq blockID and a deep-link back into the Markdown source
-
Generates a report of βorphanβ Outlook events (those whose blockIDs no longer appear in your LogSeq files), saving it to
outlook_orphans_report.txt
.(for now not working)
-
Saves updated blockID data back to
outlook_blockids.json
and opens newly created events in Outlook for review.
Supported OS & Application
- Operating System: Windows (relies on the
pywin32
COM interface) - Calendar Program: Microsoft Outlook, via
win32com.client
Startup Parameters to Customize
These are read from (or initially written to) config.json
:
graph_name
: your LogSeq graph identifier (used when constructing deep-links)logseq_dir
: full path to your local LogSeq vault (e.g.,D:\LogSeq
)days_lookahead
: how many days into the future to scan for and sync scheduled items (default: 365)
# Logseq to Outlook Events Sync Script
# Version 1.4.21 modified to open new events at the end
import os
import re
import json
from datetime import datetime, timedelta
import win32com.client
# --------------------------- Configuration Handling ---------------------------
SCRIPT_VERSION = "v1.4.21"
CONFIG_FILE = "config.json"
def load_config():
print("π§ Loading configuration...")
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
else:
config = {
"graph_name": input("Enter your LogSeq graph name: "),
"logseq_dir": input("Enter your LogSeq directory (e.g., D:\\LogSeq): "),
"days_lookahead": int(input("Enter number of days to look ahead for events: ") or "365")
}
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4)
return config
config = load_config()
GRAPH_NAME = config["graph_name"]
LOGSEQ_DIR = config["logseq_dir"]
DAYS_LOOKAHEAD = config["days_lookahead"]
print(f"π Starting LogSeq β Outlook script | Version: {SCRIPT_VERSION}\n")
# --------------------------- Pattern Definitions ---------------------------
SCHEDULED_PATTERN = re.compile(r"SCHEDULED:\s*<(\d{4}-\d{2}-\d{2})(?: \w{3})?(?: (\d{2}:\d{2}))?>")
BLOCKID_PATTERN = re.compile(r"blockID::\s*([\w-]+)")
# --------------------------- File Paths ---------------------------
script_dir = os.path.dirname(os.path.abspath(__file__))
json_file_path = os.path.join(script_dir, "outlook_blockids.json")
report_file_path = os.path.join(script_dir, "outlook_orphans_report.txt")
# --------------------------- Outlook Setup ---------------------------
outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI")
# --------------------------- Load Existing BlockIDs ---------------------------
if os.path.exists(json_file_path):
with open(json_file_path, "r", encoding="utf-8") as f:
saved_blockids_data = json.load(f)
else:
saved_blockids_data = {}
if isinstance(saved_blockids_data, list):
saved_blockids_data = {bid: "2000-01-01" for bid in saved_blockids_data}
now = datetime.now()
past_limit = now - timedelta(days=DAYS_LOOKAHEAD)
saved_blockids = {
bid: ds for bid, ds in saved_blockids_data.items()
if datetime.strptime(ds, "%Y-%m-%d") >= past_limit
}
print(f"π Loaded {len(saved_blockids)} saved blockIDs (after cleanup)\n")
# --------------------------- Read Outlook Events ---------------------------
def get_outlook_blockids():
print("π Fetching existing Outlook events...")
calendar = namespace.GetDefaultFolder(9)
items = calendar.Items
items.IncludeRecurrences = True
items.Sort("[Start]")
now = datetime.now()
future_limit = now + timedelta(days=DAYS_LOOKAHEAD)
restriction = (
f"[Start] >= '{now.strftime('%m/%d/%Y')}' "
f"AND [Start] <= '{future_limit.strftime('%m/%d/%Y')}'"
)
future_items = items.Restrict(restriction)
blockids = {}
for appt in future_items:
try:
body = getattr(appt, "Body", "")
match = re.search(r"LogSeq blockID: ([\w-]+)", body)
if match:
bid = match.group(1)
blockids[bid] = (appt.Start.Format("%Y-%m-%d %H:%M"), appt.EntryID)
except Exception as e:
print(f"β οΈ Error reading Outlook event: {e}")
return blockids
outlook_blockids = get_outlook_blockids()
logseq_blockids = set()
# Now let's save entry_id too
created_events_this_run = [] # list of tuples: (title, date, time, block_id, entry_id)
# --------------------------- Utility: Clean Markdown Block ---------------------------
def clean_block_text(text):
print(f"π Cleaning block text: {text[:30]}...")
text = re.sub(r"^\s*-\s*", "", text)
text = re.split(r"SCHEDULED:", text)[0]
text = re.sub(r":LOGBOOK:.*?:END:", "", text, flags=re.DOTALL)
text = re.sub(r"CLOCK:.*?\]", "", text, flags=re.DOTALL)
text = re.sub(r"DEADLINE:\s*<.*?>", "", text)
text = re.sub(r"logseq.order-list-type::\s*\w+", "", text)
text = re.sub(r"\w+::\s*\S+", "", text)
return text.strip()
# --------------------------- Create New Outlook Event ---------------------------
def create_event(title, scheduled_date, scheduled_time, block_id, source_file_path):
print(f"π Attempting to create event: {title} | {scheduled_date} {scheduled_time} | blockID: {block_id}")
now = datetime.now()
if block_id in saved_blockids or block_id in outlook_blockids:
print(f"β© Skipping duplicate: {title} | blockID: {block_id}")
return
dt = datetime.strptime(f"{scheduled_date} {scheduled_time or '09:00'}", "%Y-%m-%d %H:%M")
if dt <= now:
print(f"β© Ignored (past): {dt}")
return
appt = outlook.CreateItem(1)
appt.Start = dt
appt.Duration = 60
appt.ReminderSet = True
appt.ReminderMinutesBeforeStart = 10
appt.AllDayEvent = False
appt.Subject = title
logseq_link = f"logseq://graph/{GRAPH_NAME}?block-id={block_id}"
appt.Body = (
f"Automatically created from LogSeq on {now.strftime('%Y-%m-%d')} at {now.strftime('%H:%M')}\n\n"
f"LogSeq blockID: {block_id}\n\n"
f"Open in LogSeq: {logseq_link}\n"
f"[To create the link: select the LogSeq link above > copy > Ctrl+K > paste the link into the address field > confirm]\n\n"
f"LogSeq MD source file: {source_file_path}\n"
"_________________________________________________________________________________"
)
appt.Save()
entry_id = appt.EntryID
# I insert the link in the source file
def add_outlook_link(filepath, block_id, entry_id, title):
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if block_id in line:
html_link = (
f"\n\t- Link to Outlook event (entryID): "
f"<a href=\"outlook:{entry_id}\">{title}</a>"
)
lines.insert(i + 3, html_link)
break
with open(filepath, "w", encoding="utf-8") as f:
f.writelines(lines)
print(f"π Added Outlook link under blockID {block_id} in {filepath}")
add_outlook_link(source_file_path, block_id, entry_id, title)
print(f"β Created Outlook event for: {title} | blockID: {block_id}")
# Creation register
saved_blockids[block_id] = dt.strftime("%Y-%m-%d")
created_events_this_run.append((title, scheduled_date, scheduled_time or '09:00', block_id, entry_id))
# --------------------------- Scan LogSeq Directory ---------------------------
print(f"π Starting scan of LogSeq directory: {LOGSEQ_DIR}")
for root, dirs, files in os.walk(LOGSEQ_DIR):
dirs[:] = [d for d in dirs if d not in ['.recycle', 'bak', 'logseq']]
for filename in files:
if not filename.endswith(".md"):
continue
filepath = os.path.join(root, filename)
print(f"π Scanning: {filepath}")
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
block_text = ""
scheduled_date = None
scheduled_time = None
block_id = None
for line in lines:
m = BLOCKID_PATTERN.search(line)
if m:
block_id = m.group(1)
logseq_blockids.add(block_id)
if line.strip().startswith("- ") and len(line.strip()) > 1:
if scheduled_date and block_id:
title = clean_block_text(block_text)
create_event(title, scheduled_date, scheduled_time, block_id, filepath)
block_text = line.strip()
scheduled_date = None
scheduled_time = None
block_id = None
else:
block_text += " " + line.strip()
sm = SCHEDULED_PATTERN.search(line)
if sm:
scheduled_date = sm.group(1)
scheduled_time = sm.group(2)
# last block
if scheduled_date and block_id:
title = clean_block_text(block_text)
create_event(title, scheduled_date, scheduled_time, block_id, filepath)
# --------------------------- Orphans & Save ---------------------------
orphans = {
bid: details for bid, details in outlook_blockids.items()
if bid not in logseq_blockids and
bid not in [b[3] for b in created_events_this_run]
}
if orphans:
with open(report_file_path, "w", encoding="utf-8") as report:
report.write("Orphan Outlook events:\n")
for bid, (ev_time, eid) in orphans.items():
line = f" - blockID: {bid} | Date: {ev_time} | EntryID: {eid}\n"
print(line.strip())
report.write(line)
print(f"\nπ Orphan report saved to '{report_file_path}'")
with open(json_file_path, "w", encoding="utf-8") as f:
json.dump(saved_blockids, f, indent=4)
print(f"\nπ Saved {len(saved_blockids)} blockIDs to '{json_file_path}'")
# --------------------------- Summary & Open Events ---------------------------
print("β
Summary of events created in this run:")
if created_events_this_run:
for title, date, time, bid, eid in created_events_this_run:
print(f" - {title} | {date} {time} | blockID: {bid} | EntryID: {eid}")
print("\nπ Opening newly created events in Outlook...")
for _, _, _, _, eid in created_events_this_run:
try:
ev = namespace.GetItemFromID(eid)
ev.Display()
except Exception as e:
print(f"β οΈ Could not open event {eid}: {e}")
else:
print(" - No new events created")
if not orphans:
print("\nβΉοΈ No orphan events found in Outlook.")