✅ Introduction

The Org File Modification API provides a TypeScript interface for programmatically reading, modifying, and writing org-mode files. This is the VS Code equivalent of Emacs org-mode's ability to traverse and modify parse trees using elisp.

Use this API in TypeScript source blocks to:

  • Bulk update TODO states across files

  • Extract and transform data from tables

  • Rearrange document structure

  • Generate reports from org data

  • Automate repetitive org file operations

Quick Start

Basic Example: Change All TODOs to DONE

import { org } from 'scimax';

const doc = org.parseFile('./tasks.org');

org.mapHeadlines(doc, h => {
  if (h.properties.todoKeyword === 'TODO') {
    org.setTodo(h, 'DONE');
  }
});

org.writeFile('./tasks.org', doc);

Query and Report

import { org } from 'scimax';

const doc = org.parseFile('./projects.org');
const todos = org.query(doc, { hasTodo: true, todoType: 'todo' });

console.log(`Found ${todos.length} pending tasks:`);
todos.forEach(h => {
  console.log(`  - ${h.properties.rawValue}`);
});

API Reference

File I/O

org.parseFile(path, config?)

Parse an org file from disk into an AST.

const doc = org.parseFile('./notes.org');
const doc = org.parseFile('/absolute/path/to/file.org');

org.parse(content, config?)

Parse org content from a string.

const doc = org.parse('* TODO My task\nSome content');

org.writeFile(path, doc, options?)

Write a document AST back to a file.

org.writeFile('./notes.org', doc);

org.serialize(doc, options?)

Convert a document AST to an org-mode string.

const orgText = org.serialize(doc);
console.log(orgText);

Headline Traversal

org.mapHeadlines(doc, callback)

Visit every headline in the document (depth-first order).

The callback receives:

  • `headline' - The current headline element

  • `parent' - Parent headline or document root

  • `index' - Position among siblings

org.mapHeadlines(doc, (headline, parent, index) => {
  console.log(`${headline.properties.level}: ${headline.properties.rawValue}`);
});

org.filterHeadlines(doc, predicate)

Return headlines matching a predicate function.

const todos = org.filterHeadlines(doc, h =>
  h.properties.todoKeyword === 'TODO'
);

org.findHeadline(doc, predicate)

Find the first headline matching a predicate.

const intro = org.findHeadline(doc, h =>
  h.properties.rawValue === 'Introduction'
);

org.getAllHeadlines(doc)

Get all headlines as a flat array.

const all = org.getAllHeadlines(doc);
console.log(`Document has ${all.length} headlines`);

Element Traversal

org.mapElements(doc, elementType, callback)

Visit all elements of a specific type.

org.mapElements(doc, 'src-block', (block, parent, index) => {
  console.log(`Found ${block.properties.language} block`);
});

org.filterElements(doc, elementType)

Filter elements by type.

const tables = org.filterElements(doc, 'table');

org.getSrcBlocks(doc)

Get all source blocks.

const blocks = org.getSrcBlocks(doc);
blocks.forEach(b => console.log(b.properties.language));

org.getTables(doc)

Get all tables.

const tables = org.getTables(doc);

Query API

org.query(doc, criteria)

Find elements matching multiple criteria at once.

Query Criteria

PropertyTypeDescription
typeElementTypeElement type to match
todoKeywordstring or string[]Specific TODO keyword(s)
hasTodobooleanHas any TODO keyword
todoType'todo' or 'done'TODO type category
tagsstring[]All these tags must be present
anyTagstring[]At least one of these tags
levelnumberExact headline level
minLevelnumberMinimum headline level
maxLevelnumberMaximum headline level
titleContainsstringTitle contains text (case-insensitive)
hasPropertystringHas property with this key
property{key, value}Property equals value
languagestringSource block language
predicatefunctionCustom filter function

Examples

// Find all TODO headlines with :project: tag
const projects = org.query(doc, {
  type: 'headline',
  hasTodo: true,
  tags: ['project']
});

// Find headlines at level 2 or 3
const sections = org.query(doc, {
  type: 'headline',
  minLevel: 2,
  maxLevel: 3
});

// Find Python source blocks
const pythonBlocks = org.query(doc, {
  type: 'src-block',
  language: 'python'
});

// Find headlines with CATEGORY property set to 'work'
const work = org.query(doc, {
  type: 'headline',
  property: { key: 'CATEGORY', value: 'work' }
});

// Custom predicate
const long = org.query(doc, {
  type: 'headline',
  predicate: h => h.properties.rawValue.length > 50
});

Table Utilities

org.tableToJSON(table, options?)

Convert an org table to JSON.

Options

OptionDefaultDescription
useHeadertrueUse first row as object keys
trimtrueTrim whitespace from cells
includeRulesfalseInclude horizontal rule rows
const tables = org.getTables(doc);
const data = org.tableToJSON(tables[0]);
// Returns: [{ name: 'Alice', age: '30' }, { name: 'Bob', age: '25' }]

org.jsonToTable(data, options?)

Convert JSON data to org table text.

const table = org.jsonToTable([
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 25 }
]);
// Returns:
// | name  | age |
// |-------+-----|
// | Alice | 30  |
// | Bob   | 25  |

org.tableToCSV(table, options?)

Convert an org table to CSV format.

Options

OptionDefaultDescription
useHeadertrueInclude header row in output
trimtrueTrim whitespace from cells
delimiter','Field separator character
quote'"'Quote character for escaping
lineEnding'\n'Line ending character(s)
const tables = org.getTables(doc);
const csv = org.tableToCSV(tables[0]);
// Returns:
// name,age
// Alice,30
// Bob,25

// With custom delimiter (TSV)
const tsv = org.tableToCSV(tables[0], { delimiter: '\t' });

// With Windows line endings
const csvWin = org.tableToCSV(tables[0], { lineEnding: '\r\n' });

Fields containing the delimiter, quotes, or newlines are automatically escaped:

// Input: | name | note |
//        | Bob  | hello, world |
const csv = org.tableToCSV(table);
// Output: name,note
//         Bob,"hello, world"

org.writeTableToCSV(filePath, table, options?)

Write a table directly to a CSV file.

const tables = org.getTables(doc);
org.writeTableToCSV('./data.csv', tables[0]);

// With semicolon delimiter
org.writeTableToCSV('./data.csv', tables[0], { delimiter: ';' });

Modification Helpers

org.setTodo(headline, keyword, doneKeywords?)

Set or remove TODO keyword.

org.setTodo(headline, 'TODO');     // Set to TODO
org.setTodo(headline, 'DONE');     // Set to DONE
org.setTodo(headline, undefined);  // Remove TODO

org.addTag(headline, tag)

Add a tag to a headline.

org.addTag(headline, 'important');

org.removeTag(headline, tag)

Remove a tag from a headline.

org.removeTag(headline, 'obsolete');

org.setProperty(headline, key, value)

Set a property on a headline.

org.setProperty(headline, 'CUSTOM_ID', 'my-section');

org.removeProperty(headline, key)

Remove a property from a headline.

org.removeProperty(headline, 'OLD_ID');

org.setPriority(headline, priority)

Set or remove priority.

org.setPriority(headline, 'A');     // Set priority
org.setPriority(headline, undefined); // Remove priority

Structure Utilities

org.sortHeadlines(headlines, compareFn)

Sort headlines in place.

// Sort by priority
org.sortHeadlines(doc.children, (a, b) => {
  const priority = { A: 0, B: 1, C: 2 };
  return (priority[a.properties.priority] || 99) -
         (priority[b.properties.priority] || 99);
});

// Sort alphabetically
org.sortHeadlines(doc.children, (a, b) =>
  a.properties.rawValue.localeCompare(b.properties.rawValue)
);

org.promoteHeadline(headline, recursive?)

Decrease headline level (promote).

org.promoteHeadline(headline);        // Promote with children
org.promoteHeadline(headline, false); // Promote only this headline

org.demoteHeadline(headline, recursive?)

Increase headline level (demote).

org.demoteHeadline(headline);

Tree Manipulation

org.createHeadline(title, level?, options?)

Create a new headline element.

// Basic headline
const h = org.createHeadline('New Section', 2);

// With TODO, priority, and tags
const task = org.createHeadline('Important Task', 1, {
  todoKeyword: 'TODO',
  todoType: 'todo',
  priority: 'A',
  tags: ['urgent', 'work'],
  properties: { CUSTOM_ID: 'my-task' }
});

org.insertHeadline(parent, headline, index?)

Insert a headline into a document or parent headline.

const newHeadline = org.createHeadline('New Section', 1);

// Insert at end
org.insertHeadline(doc, newHeadline);

// Insert at specific position
org.insertHeadline(doc, newHeadline, 0); // At beginning

// Insert as child (level auto-adjusts)
org.insertHeadline(parentHeadline, newHeadline);

org.deleteHeadline(parent, headline)

Delete a headline from its parent.

// Delete by reference
const deleted = org.deleteHeadline(doc, doc.children[0]);

// Delete by index
const deleted = org.deleteHeadline(doc, 2);

org.copyHeadline(headline)

Create a deep copy of a headline and its children.

const original = doc.children[0];
const copy = org.copyHeadline(original);

// Modify copy without affecting original
copy.properties.rawValue = 'Modified Title';
org.insertHeadline(doc, copy);

org.findParent(doc, headline)

Find the parent of a headline (document or headline).

const parent = org.findParent(doc, headline);
if (parent.type === 'org-data') {
  console.log('Top-level headline');
} else {
  console.log('Child of:', parent.properties.rawValue);
}

org.getHeadlinePath(doc, headline)

Get the path from root to a headline.

const path = org.getHeadlinePath(doc, deepHeadline);
// Returns: [Level1Headline, Level2Headline, targetHeadline]
console.log(path.map(h => h.properties.rawValue).join(' > '));

Timestamp Utilities

org.createTimestamp(options)

Create a timestamp object.

// Basic date
const ts = org.createTimestamp({ year: 2024, month: 3, day: 15 });
// <2024-03-15>

// With time
const ts = org.createTimestamp({
  year: 2024, month: 3, day: 15,
  hour: 14, minute: 30
});
// <2024-03-15 14:30>

// Inactive timestamp
const ts = org.createTimestamp({
  year: 2024, month: 3, day: 15,
  active: false
});
// [2024-03-15]

// With repeater
const ts = org.createTimestamp({
  year: 2024, month: 3, day: 15,
  repeaterType: '+',
  repeaterValue: 1,
  repeaterUnit: 'w'
});
// <2024-03-15 +1w>

org.timestampFromDate(date, options?)

Create a timestamp from a JavaScript Date.

const ts = org.timestampFromDate(new Date());
const tsWithTime = org.timestampFromDate(new Date(), { includeTime: true });

org.timestampToDate(timestamp)

Convert a timestamp to a JavaScript Date.

const date = org.timestampToDate(headline.planning.scheduled);
console.log(date.toLocaleDateString());

org.setScheduled(headline, timestamp)

Set the SCHEDULED timestamp on a headline.

const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
org.setScheduled(headline, org.timestampFromDate(tomorrow));

// Remove scheduled
org.setScheduled(headline, undefined);

org.setDeadline(headline, timestamp)

Set the DEADLINE timestamp on a headline.

const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);
org.setDeadline(headline, org.timestampFromDate(nextWeek));

org.setClosed(headline, timestamp)

Set the CLOSED timestamp on a headline.

org.setTodo(headline, 'DONE');
org.setClosed(headline, org.timestampFromDate(new Date(), { active: false }));

org.getScheduled / org.getDeadline / org.getClosed

Get planning timestamps from a headline.

const scheduled = org.getScheduled(headline);
const deadline = org.getDeadline(headline);
const closed = org.getClosed(headline);

if (deadline) {
  const daysUntil = Math.ceil(
    (org.timestampToDate(deadline) - new Date()) / (1000 * 60 * 60 * 24)
  );
  console.log(`Due in ${daysUntil} days`);
}

Clock Utilities

org.getClockEntries(headline)

Get all clock entries from a headline.

const clocks = org.getClockEntries(headline);
clocks.forEach(clock => {
  console.log(`Duration: ${clock.properties.duration}`);
});

org.getAllClockEntries(doc)

Get all clock entries from a document with their parent headlines.

const entries = org.getAllClockEntries(doc);
entries.forEach(({ clock, headline }) => {
  console.log(`${headline.properties.rawValue}: ${clock.properties.duration}`);
});

org.getTotalClockTime(headline, recursive?)

Calculate total clocked time in minutes.

const minutes = org.getTotalClockTime(headline);
console.log(`Time spent: ${org.formatDuration(minutes)}`);

// Include children
const totalWithChildren = org.getTotalClockTime(headline, true);

org.formatDuration(minutes)

Format minutes as HH:MM string.

org.formatDuration(90);  // "1:30"
org.formatDuration(125); // "2:05"

Property Inheritance

org.getInheritedProperty(doc, headline, key)

Get a property value, checking ancestors if not set on headline.

// Will check headline, then parents, then document
const category = org.getInheritedProperty(doc, headline, 'CATEGORY');

org.getEffectiveProperties(doc, headline)

Get all properties including inherited ones.

const props = org.getEffectiveProperties(doc, headline);
console.log(props['CATEGORY']); // From nearest ancestor
console.log(props['CUSTOM_ID']); // From headline itself

Complete Examples

Archive All DONE Items

import { org } from 'scimax';

const doc = org.parseFile('./tasks.org');

org.mapHeadlines(doc, h => {
  if (h.properties.todoType === 'done') {
    org.addTag(h, 'ARCHIVE');
  }
});

org.writeFile('./tasks.org', doc);

Generate Project Summary

import { org } from 'scimax';

const doc = org.parseFile('./projects.org');
const projects = org.query(doc, { tags: ['project'] });

console.log('# Project Summary\n');

for (const project of projects) {
  const status = project.properties.todoKeyword || 'No status';
  const priority = project.properties.priority || '-';
  console.log(`- [${priority}] ${project.properties.rawValue} (${status})`);
}

Extract Table Data Across Files

import { org } from 'scimax';
import * as fs from 'fs';

const files = fs.readdirSync('.').filter(f => f.endsWith('.org'));
const allData = [];

for (const file of files) {
  const doc = org.parseFile(file);
  const tables = org.getTables(doc);

  for (const table of tables) {
    const data = org.tableToJSON(table);
    allData.push(...data);
  }
}

return allData;

Export Tables to CSV Files

import { org } from 'scimax';

const doc = org.parseFile('./data.org');
const tables = org.getTables(doc);

// Export first table to CSV
org.writeTableToCSV('./output/table1.csv', tables[0]);
console.log('Exported table1.csv');

// Export all tables with custom options
tables.forEach((table, i) => {
  org.writeTableToCSV(`./output/table${i + 1}.csv`, table, {
    delimiter: ';',      // Semicolon for Excel compatibility
    lineEnding: '\r\n'   // Windows line endings
  });
});

console.log(`Exported ${tables.length} tables`);

Export Named Table

import { org } from 'scimax';
import * as fs from 'fs';

const doc = org.parseFile('./report.org');

// Find a table by the headline containing it
const dataSection = org.findHeadline(doc, h =>
  h.properties.rawValue === 'Results Data'
);

if (dataSection?.section) {
  const table = dataSection.section.children.find(
    el => el.type === 'table'
  );

  if (table) {
    const csv = org.tableToCSV(table);
    fs.writeFileSync('./results.csv', csv);
    console.log('Exported results.csv');
  }
}

Bulk Update Properties

import { org } from 'scimax';

const doc = org.parseFile('./notes.org');

org.mapHeadlines(doc, h => {
  // Add CREATED property if missing
  if (!h.propertiesDrawer?.['CREATED']) {
    org.setProperty(h, 'CREATED', new Date().toISOString().split('T')[0]);
  }

  // Set CATEGORY based on tags
  if (h.properties.tags.includes('work')) {
    org.setProperty(h, 'CATEGORY', 'Work');
  } else if (h.properties.tags.includes('personal')) {
    org.setProperty(h, 'CATEGORY', 'Personal');
  }
});

org.writeFile('./notes.org', doc);

Reorganize by Priority

import { org } from 'scimax';

const doc = org.parseFile('./tasks.org');

// Sort top-level headlines by priority
org.sortHeadlines(doc.children, (a, b) => {
  const priorityOrder = { A: 0, B: 1, C: 2 };
  const aPriority = priorityOrder[a.properties.priority] ?? 99;
  const bPriority = priorityOrder[b.properties.priority] ?? 99;
  return aPriority - bPriority;
});

org.writeFile('./tasks.org', doc);

Schedule All Unscheduled TODOs

import { org } from 'scimax';

const doc = org.parseFile('./tasks.org');

// Find unscheduled TODOs
const unscheduled = org.query(doc, {
  hasTodo: true,
  todoType: 'todo'
}).filter(h => !org.getScheduled(h) && !org.getDeadline(h));

// Schedule them for tomorrow
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const ts = org.timestampFromDate(tomorrow);

unscheduled.forEach(h => org.setScheduled(h, ts));

org.writeFile('./tasks.org', doc);
console.log(`Scheduled ${unscheduled.length} tasks for tomorrow`);

Generate Time Report

import { org } from 'scimax';

const doc = org.parseFile('./work.org');

console.log('# Time Report\n');

org.mapHeadlines(doc, h => {
  const time = org.getTotalClockTime(h, true);
  if (time > 0) {
    const indent = '  '.repeat(h.properties.level - 1);
    console.log(`${indent}- ${h.properties.rawValue}: ${org.formatDuration(time)}`);
  }
});

Clone Template Headline

import { org } from 'scimax';

const doc = org.parseFile('./projects.org');

// Find template headline
const template = org.findHeadline(doc, h =>
  h.properties.tags.includes('template')
);

if (template) {
  // Create new project from template
  const newProject = org.copyHeadline(template);
  newProject.properties.rawValue = 'New Project';
  org.removeTag(newProject, 'template');
  org.setTodo(newProject, 'TODO');
  org.setProperty(newProject, 'CREATED', new Date().toISOString().split('T')[0]);

  org.insertHeadline(doc, newProject, 0);
  org.writeFile('./projects.org', doc);
}

Move Completed Tasks to Archive

import { org } from 'scimax';

const doc = org.parseFile('./tasks.org');

// Find or create archive headline
let archive = org.findHeadline(doc, h =>
  h.properties.rawValue === 'Archive'
);

if (!archive) {
  archive = org.createHeadline('Archive', 1);
  org.insertHeadline(doc, archive);
}

// Find and move done items
const done = org.query(doc, { todoType: 'done' })
  .filter(h => !h.properties.tags.includes('ARCHIVE'));

done.forEach(h => {
  const parent = org.findParent(doc, h);
  if (parent && parent !== archive) {
    org.deleteHeadline(parent, h);
    org.addTag(h, 'ARCHIVE');
    org.insertHeadline(archive, h);
  }
});

org.writeFile('./tasks.org', doc);
console.log(`Archived ${done.length} completed tasks`);

Headline Element Structure

When working with headlines, you have access to these properties:

interface HeadlineElement {
  type: 'headline';
  properties: {
    level: number;           // 1-based heading level
    rawValue: string;        // Title text
    todoKeyword?: string;    // 'TODO', 'DONE', etc.
    todoType?: 'todo' | 'done';
    priority?: string;       // 'A', 'B', 'C'
    tags: string[];          // Tag list
    archivedp: boolean;      // Has :ARCHIVE: tag
    commentedp: boolean;     // Has COMMENT prefix
    customId?: string;       // CUSTOM_ID property
    id?: string;             // ID property
    category?: string;       // CATEGORY property
    lineNumber: number;      // 1-indexed line number
  };
  propertiesDrawer?: Record<string, string>;
  planning?: PlanningElement;  // SCHEDULED, DEADLINE, CLOSED
  section?: SectionElement;    // Content after headline
  children: HeadlineElement[]; // Child headlines
}

Navigation