Exporting and Importing Timing Time Entries

Timing now offers a proper built-in sync solution, so in many cases that option is preferable. However, you can still use these scripts to export your time entries. For more information, see this article.

Simply copy one of the following script into a new "Script Editor" document, select "JavaScript" as the script's language, and press run (see this article for more detailed instructions).
After pasting them, consider saving the scripts to disk for future re-use. You could even save them to iCloud Drive (or e.g. Dropbox or Google Drive) to have them available on all your Macs.
Feel free to customize the scripts for your use cases; if you need details on how a particular method works, please refer to our AppleScript reference.

Note: We do not take responsibility for any data loss incurred by running these scripts.
Make sure to back up your data (e.g. by copying the directories mentioned here to a different location) before running these scripts.

Exporting All Timing Time Entries in the Past 30 Days to a CSV File

var endDate = new Date();
var startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - 30 /* days */);

// Copyright (c) 2024 Timing Software GmbH. All rights reserved.
// This script is licensed only to extend the functionality of Timing. Redistribution and any other uses are not allowed without prior permission from us.
var helper = Application("TimingHelper");
if (!helper.advancedScriptingSupportAvailable()) { throw "This script requires a Timing Connect subscription. Please contact support via https://timingapp.com/contact to upgrade."; }
var app = Application.currentApplication();
app.includeStandardAdditions = true;

var taskListPath = app.chooseFileName({ withPrompt: "Select which file to write the Time Entries to.", defaultName: "TimingTasks.csv" }).toString();

$.NSFileManager.defaultManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError($(taskListPath).stringByDeletingLastPathComponent.stringByStandardizingPath, true, $(), $());

var reportSettings = helper.ReportSettings().make();
var exportSettings = helper.ExportSettings().make();

reportSettings.firstGroupingMode = "raw";
reportSettings.timeEntriesIncluded = true;
reportSettings.appUsageIncluded = false;

exportSettings.fileFormat = "CSV";
exportSettings.durationFormat = "seconds";
exportSettings.shortEntriesIncluded = true;

var app = Application.currentApplication();
app.includeStandardAdditions = true;

helper.saveReport({ withReportSettings: reportSettings, exportSettings: exportSettings, between: startDate, and: endDate, to: Path($(taskListPath).stringByStandardizingPath.js) });

helper.delete(reportSettings);
helper.delete(exportSettings);

Importing Time Entries from a File into Timing

// Copyright (c) 2024 Timing Software GmbH. All rights reserved.
// This script is licensed only to extend the functionality of Timing. Redistribution and any other uses are not allowed without prior permission from us.
var helper = Application("TimingHelper");
if (!helper.advancedScriptingSupportAvailable()) { throw "This script requires a Timing Connect subscription. Please contact support via https://timingapp.com/contact to upgrade."; }
var app = Application.currentApplication();
app.includeStandardAdditions = true;

app.displayDialog("Before proceeding, please make sure to back up your Timing database.\n\nSee https://timingapp.com/help/faq#data on which folders you need to back up.");

var taskListPath = app.chooseFile({ withPrompt: "Select which CSV file to read the Time Entries from.", ofType: ["public.comma-separated-values-text"] }).toString();

// Minimal ES6 CSV parser.
// Taken from: https://lowrey.me/parsing-a-csv-file-in-es6-javascript/
var Csv = null;
Csv = class {
  parseLine(text) {
    const regex =
    /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
    let arr = [];
    text.replace(regex, (m0, m1, m2, m3) => {
      if (m1 !== undefined) {
        arr.push(m1.replace(/\\'/g, "'"));
      } else if (m2 !== undefined) {
        arr.push(m2.replace(/\\"/g, "\""));
      } else if (m3 !== undefined) {
        arr.push(m3);
      }
      return "";
    });
    if (/,\s*$/.test(text)) {
      arr.push("");
    }
    return arr;
  }

  zipObject(props, values) {
    return props.reduce((prev, prop, i) => {
      prev[prop] = values[i];
      return prev;
    }, {});
  }

  parse(csv) {
    let [properties, ...data] = csv.split("\n").map(this.parseLine);
    return data.map((line) => this.zipObject(properties, line))
  };

  serialize(obj) {
    let fields = Object.keys(obj[0]);
    let csv = obj.map(row => fields.map((fieldName) => JSON.stringify(row[fieldName] || "")));
    return [fields, ...csv].join("\n");
  }; 
}

function createProjectWithNameChainElements(projectNameChainElements) {
	var currentProject = null;
	var currentSearchSpace = helper.rootProjects();
	for (var projectName of projectNameChainElements) {
		if (!projectName) continue;
		
		var newProject = currentSearchSpace.filter(function(project) { return project.name() == projectName; })[0];
		if (!newProject) {
			var arguments = { name: projectName };
			if (currentProject) {
				arguments["parentProject"] = currentProject;
			}
			newProject = helper.createProject(arguments);
		}
		currentProject = newProject;
		currentSearchSpace = currentProject.projects();
	}
	return currentProject;
}

function createProjectWithNameChain(projectNameChain) {
	return createProjectWithNameChainElements(projectNameChain.split(" ▸ "));
}

var csvLines = $.NSString.stringWithContentsOfFileEncodingError(
	$(taskListPath).stringByStandardizingPath,
	$.NSUTF8StringEncoding,
	$()
).js.split("\n");

csvLines.shift();
var csv = new Csv();
for (var csvLine of csvLines) {
	if (!csvLine) continue;
	
	var csvFields = csv.parseLine(csvLine);
	var taskStartDate = new Date(csvFields[2]);
	var taskEndDate = new Date(csvFields[3]);
	var taskProjectNameChain = csvFields[4];
	var taskDescription = csvFields[5];
	var taskNotes = csvFields[6];
	
	if (!taskStartDate || !taskEndDate || (taskEndDate.getTime() - taskStartDate.getTime()) < 1000) continue;
	
	var arguments = { from: taskStartDate, to: taskEndDate };
	if (taskDescription) arguments["withTitle"] = taskDescription;
	if (taskProjectNameChain) {
		var taskProject = createProjectWithNameChain(taskProjectNameChain);
		if (taskProject) arguments["project"] = taskProject;
	}
	if (taskNotes) arguments["notes"] = taskNotes;
	helper.addTimeEntry(arguments);
}

Take our free 5-day course to get started with Timing.