Time Tracking with Plain Text and Drafts - Summary

I track my time using plain text and Drafts. Detail about how I do this can be found in my prior posts:

You can download the actions I use to make this easier from the Drafts Action Directory here. Below is a very brief explanation of the included actions. After downloading, you may want to move these to different groups. The actions included in the download are:

insert date - Just inserts the date in a format I like. The key part here is that it includes the date in iso format (i.e. 2020-02-14).

insert time - Inserts the current time rounded to the nearest 6-minute increment. Feel free to modify this for different time increments, or to get rid of the rounding entirely.

start timeslip - Just a convenience action that creates a new timeslip with the date and the current time and a “time” tag. I call this action via a Shortcut that triggers when I get to my office, thereby reminding me to start time tracking.

get client number - A very useful front end for the getClient action. This takes any selected text and looks for a corresponding client. If no text is selected, you get a box where you can type in a client name or alias. Handy for looking up that billing number quickly.

process timeslip - This is the big action that takes the plain text time slip format and turns it into useful bits of information, including a nicely formatted email and some bits to append to tracking files.

PLEASE NOTE that you’ll need to manually add headers to the the tracking files. This only needs to be done once, and is probably easiest after you run process timeslip for the first time. After you’ve done that, find the time.txt file in the root level of the Drafts iCloud folder, and add the following as the first line of the text in that file using whatever text editor you prefer:

date|duration|client|project|note|billable

Do the same thing with the time.txt file in the root level of your Dropbox.

And finally, open the time.csv file in the root level of your OneDrive with a text editor (don’t use Excel) and add the following as the first line:

date,duration,client,project,note,billable

If you don’t plan on using OneDrive or Dropbox, you can go ahead and delete those steps from the process timeslip action.

time report - This action reads the time.txt file from the Drafts folder in iCloud, and creates a summary, showing totals for your billable, non-billable, and overall time for the current and prior week, month, and year as a nice HTML preview.

getClient - This is the function described in detail here, that is relied on by the process timeslip and get client number actions. There’s no reason to activate this action directly. It just needs to be available for those other actions to call.

dumbify - Just a utility function to get ride of curly quotes and em and en dashes since some of the ASCII functions don’t play nice with those characters. Again, there’s no need to activate this action directly. Just keep it somewhere.

Time Tracking Using Plaint Text and Drafts - Part 5 (the getClient function)

As mentioned in my last post, I made a getClient function to make my plain text time tracking a whole lot easier. The function allows me to standardize client names and retrieve other data related to those clients. Standardizing improves the plain text time tracking experience immensely:

First, if you don’t standardize the client name, you get a lot of entries, that should really be one. For example, ABC Corp. and ABC and abc corp and abc corp. would be treated as separate clients (since the names don’t match exactly). This annoyance can be fixed in a single time slip relatively easily. But as soon as you want to calculate a total number of hours over a week or month or year, you need to make sure all the entries for a client use exactly the same client name. And remembering that name can be really hard if the last time you worked on a matter was weeks or months ago.

Second, there’s a tension between (1) choosing a short client name that is easy to type as you record your time and (2) choosing a client name that is both distinct from other client names and descriptive enough to make sense to your assistant or future self who is trying to understand which actual client-matter an entry refers to.

The solution is to have a database, (here just a simple JSON file) to keep track of everything. Each client can have an official name and a whole bunch of “aliases,” which don’t have to be unique, along with a number, a billable flag, and any other useful information.

That’s what the getClient function does. And it’s got some bells and whistles too: It take a string, checks to see if it matches any alias of any client. If it matches more than one, it lets you choose which one you really meant. If there was a typo in a client alias in your time slip, it allows you to correct it, and if you have a new client, it allows you to add the new client to the JSON database.

Once getClient knows which client you’re actually referring to, it returns the entire client object, allowing you to access the client name, billing number, or whatever other data you might want.

I use getClient in several different actions, not just the action to process a time slip. For example, I have a keyboard button that looks up the client number for selected text (or prompts to type the text if nothing is selected). This is very helpful all those times when you need the billing number, but can’t quite remember it.

Finally, this process allows me to define a non-trackable “client” with a billing number of zero. The aliases for that client include all the things that I don’t want to capture in my time tracking—things like “lunch”, “break”, “doctor”, “dentist”, etc. The time tracking script mentioned a couple of posts ago filters out all entries where the billing number is 0.

Anyway, here’s the script:

// function that takes string returns client object with matching alias
// or prompts to correct string or add new client object if no match
// client object includes name, nickname, number, billable, alias, and description

function getClient(candidate) {
	
	const fileName =  "clientList.json";
	
	// make candidate lowercase for easy comparison
	candidate = candidate.toLowerCase().trim();
	
	// load contents of file
	myCloud = FileManager.createCloud();
	let contents = myCloud.read("/" + fileName);
	let clientList = JSON.parse(contents);
	
	// filter potential matches
	let matches = clientList.clients.filter(c => c.alias.includes(candidate));
	
	switch (matches.length) {
		
		// if one match, return match
		case 1:
			return(matches[0]);
		
		// if no match, prompt for correction or new client
		case 0:
			let p = Prompt.create();
			p.title = "no match";
			p.message = "there was no match for " + candidate + "\n\n if there was a typo, enter corrected name. If " + candidate + " is a new client, enter name, nickname, aliases (separated by a comma) and a client-number";
			p.addTextField("clientName", "Corrected/New Client Name", candidate, {wantsFocus:true});
			p.addTextField("clientNickname", "Nickname (for iCloud folder)", "");
			p.addTextField("clientAliases", "Client Aliases", "");
			p.addTextField("clientNumber", "Client Number", "", {placeholder:"00000-0"});
			p.addSwitch("billable", "Billable?", true);
			p.addTextField("description", "Description", "");
			p.addButton("Correct");
			p.addButton("Add");


			// display the prompt
			let didNotCancel = p.show();


			// if cancelled, just quit;
			if (!didNotCancel) { 
				context.cancel("user cancelled");
				return;
				
			} else {
			
			// if chose to add new client, add to list and return new info
			if (p.buttonPressed == "Add") {
				let newClientName = p.fieldValues["clientName"];
				newClientName = newClientName.toLowerCase().trim();
				let newClientNickname = p.fieldValues["clientNickname"];
				newClientNickname = newClientNickname.toLowerCase().trim();
				let alias = p.fieldValues["clientAliases"].split(",");
				alias = alias.map(x => x.toLowerCase().trim());
				alias.push(newClientName);
				alias.push(newClientNickname);
				alias = [...new Set(alias)]; // eliminate any duplicates
				let number = p.fieldValues["clientNumber"];
				let bill = p.fieldValues["billable"];
				let description = p.fieldValues["description"];
							
				// build a new client object
				let newClient = {
					name: newClientName,
					nickname: newClientNickname,
					number: number,
					billable: bill,
					alias: alias,
					description: description
				};
				
				// add new client to list
				clientList.clients.push(newClient);

				// write the updated client list to the file
				contents = JSON.stringify(clientList);
				let success = myCloud.write("/" + fileName, contents);
				if (success) {alert(candidate + " added!");}
				else {alert("error: file wasn't written");}
				return(getClient(newClientName));
			}
			
			// if corrected, re-run function with corrected candidate
			if (p.buttonPressed == "Correct") {
				console.log("correct selected. Now running getClient on: " + p.fieldValues["clientName"]);
				return(getClient(p.fieldValues["clientName"]));
			}
			}


		// if multiple matches, chose among them
		default:
			let q = Prompt.create();
			q.title = "multiple matches";
			q.message = "choose which client you meant:";
			for (c in matches) {
				q.addButton(matches[c].name);
			}
			q.show();
			let choice = matches.filter(c => c.name == (q.buttonPressed));
			return(choice[0]);
	}
}

Note that it stores the database as clientList.json in the top level of your Drafts folder in iCloud, but you can move this elsewhere and adjust the script accordingly.

Next, I’ll be putting together a tl;dr version of all this, with links to download the actions to the action directory.

Time Tracking Using Plain Text and Drafts - Part 4 (More Processing)

To recap: we’ve turned our plain text time slip into a list of time entries, each with a date, duration, client, project, note, billing number, and billable/non-billable flag. But now we need to clean up the data.

First, we need a function to combine entries that have the same client and project (like the entries for ABC Corp. in our example time slip).

function combineEntries(dictList) {
	let result = [];
	result[0] = dictList[0];
	for (i = 1; i < dictList.length; i++) {		let matched = false;
		for (e in result) {
			if (result[e].number == dictList[i].number && result[e].project == dictList[i].project) {
				result[e].duration = Number(result[e].duration) + Number(dictList[i].duration);
				result[e].duration = result[e].duration.toFixed(1);
				result[e].note += " " + dictList[i].note;
				matched = true;
				break;
			}
		}
		if (!matched) {
			result.push(dictList[i]);
		}
	}
	return result;
}

(I also have a second, simpler function—called combineEntriesForEmail—that combines entries for the same client even if they don’t have the same project. I’ll include it in the final post, but won’t bother adding it here.)

To do this initial combining of entires, I just call this function on the entries list that we created in the last pose:

entries = combineEntries(entries);

Second, we’ll eliminate non-trackable entries (like lunch in the example) by filtering those out:

entries = entries.filter(x => x.number != "0");

And finally, we’ll sort the data so that billable client work comes before non-billable administrative work:

entries = entries.sort((a,b) => b.billable - a.billable);

Now that we have the data cleaned up, the next step is to make some useful strings to send elsewhere. I send my data to three primary destinations: (1) A pipe-delimited file akin to a .csv; (2) a true .csv file (so that Microsoft BI can read it without additional interference); and (3) an email to my assistant. Here’s how those are compiled:

// build pipe-delimited text file addition
var txtAddition = [];
for (e in entries) {
	txtAddition.push(entries[e].date + "|" + entries[e].duration + "|" + entries[e].client + "|" + entries[e].project + "|" + entries[e].note + "|" + entries[e].billable);
}


// build .csv addition
var csvAddition = [];
for (e in entries) {
	csvAddition.push(entries[e].date + ',' + entries[e].duration + ',' + entries[e].client + ',' + entries[e].project + ',"' + entries[e].note.replace(/"/g,'""') + '",' + entries[e].billable);
}


// consolidate entries for email and build email contents
entries = combineEntriesForEmail(entries);
var emailContents = '';
for (e in entries) {
	emailContents += entries[e].duration + "\n" + entries[e].client + " (" + entries[e].number + ")\n" + entries[e].note + "\n\n"
}

// add a total for the day
var dayTotal = 0;
for (e in entries) {
	dayTotal += Number(entries[e].duration);
}
dayTotal = dayTotal.toFixed(1);
emailContents += "----\n" + dayTotal + " total";

Finally, now that I have the strings I want, I define Drafts template tags so they can be used in subsequent action steps without all the coding.

draft.setTemplateTag('slipDate', date);
draft.setTemplateTag('emailContents', emailContents);
draft.setTemplateTag('txtAddition', txtAddition.join('\n'));
draft.setTemplateTag('csvAddition', csvAddition.join('\n'));

This lets me use an email step and get the contents for the email just by using a [[emailContents]] tag. For our sample time slip, the email contents would look like this:

2.7
ABC Corp. (1111-1)
Review lease. Review and respond to additional email question form Mr. ABC.

1.9
XYZ Co. (2222-2)
Prepare for oral argument of upcoming motion to dismiss.

3.0
Jones (3333-3)
Interview Ms. Jones regarding dispute with business partners. Prepare memo outlining potential claims.

----
7.6 total

Same goes for appending to cloud files—just use [[txtAddition]] or [[csvAddition]] as appropriate.

That’s really all there is to processing the time. In the next post, I’ll explain that getClient function I mentioned before. For me, getting the getClient function turned my system from somewhat fiddly and brittle, so one that feels rock solid and doesn’t require maintenance.

Time Tracking Using Plain Text and Drafts - Part 3 (Processing)

A plain text time slip is a good first step. We could simply treat it as paper and do the time calculations by hand (calculations made easier if we rounded each time using a button as described in the last post). But we can make Drafts do the work for us instead.

Below I’ll through the steps to turn a time slip like the one below into more useful data. I’m breaking the script I use up into a lot of pieces to help explain what it does over the course of a couple posts. But I’ll share the complete script in the final post to this series.

Here’s that time slip again:

2020-02-07 (Friday)

08:30 ABC Corp. - Review lease.

10:12 XYZ Co., motion to dismiss - Prepare for oral argument of upcoming motion to dismiss.

12:06 lunch

Had a great lunch at new sushi place.

13:00 ABC Corp. - Review and respond to addition email question from Mr. ABC.

13:30 Jones, meetings - Interview Ms. Jones regarding dispute with business partners.

14:48 Jones, strategy - Prepare memo outlining potential claims.

16:30 ABC Corp.

17:00 head home

Remember to buy wine on the way home!

We’ll begin by saving that time slip text into a constant:

const timeslip = editor.getText();

Then we’ll grab the date:

const date = timeslip.match(/^\d\d\d\d-\d\d-\d\d/g);

Then the times:

const times = timeslip.match(/^\d\d\:\d\d/gm);

Then the lines with the client, project, and description data:

var lines = timeslip.match(/^\d\d:\d\d.*\n?/gm);
lines.pop();

(We drop the last line because it just has the time we stopped working).

Now we need to turn those lines into more structured data—a list of entries, each with a date, a duration, a client, a project if any, and a description. The below bit of the script just loops through those lines, creating an entry for each:

var entries = [];
for (i = 0; i < lines.length; i++) {
	entries[i] = {}
	entries[i].date = date;
	entries[i].duration = duration(times[i],times[i+1]);
	
	let name = lines[i].match(/.+?(?=(,|\n| - ))/g)[0].slice(6).trim(); // match beginning of line, as few as possible, to comma, new line, or space-hyphen-space
	entries[i].client = getClient(name).name; // standardize the client name
	
	entries[i].project = lines[i].match(/, [\w ]+(?=( \- |\n))/g); // match from comma space until you either get a new line or an isolated hyphen 
		if (entries[i].project === null) {
			entries[i].project = '';
		} else {
			entries[i].project = entries[i].project[0].slice(2).toLowerCase().trim();
		}
	
	entries[i].note = lines[i].match(/- .*/g);   // match from hyphen space forward
		if (entries[i].note === null) {
			entries[i].note = '';
		}	else {
			entries[i].note = entries[i].note[0].slice(2).trim();
		}
	
	entries[i].billable = getClient(entries[i].client).billable;
	entries[i].number = getClient(entries[i].client).number;
}

There are a couple of functions used in there that need to be explained.

First, there’s the duration function, which takes two times and returns the duration between them. That function is as follows:

// function to calculate durations from time-based strings
function duration(start, end) {
	var h1 = start.slice(0,2);
	var h2 = end.slice(0,2);
	var hours = h2 - h1;
	var m1 = start.slice(3);
	var m2 = end.slice(3);
	var tenths = (m2 - m1)/60;
	var totalTime = hours + tenths;
	return totalTime.toFixed(1);
}

Next, there’s a getClient function that takes a client name, standardizes it, and provides data associated with that name (like a billing number and whether the matter is billable or not). This getClient function isn’t strictly necessary, but it’s very nice to have.

At the end of all that processing we have a list called entries where each entry in the list has a date, a duration, a client, a project, a note, and (as a bonus) a billing number and a flag indicating whether the matter is billable or not.

But we aren’t done yet. In the next post, we’ll go through this list of entries, combine entries with the same client and project, and filter out entries for things we aren’t tracking (like “lunch”), sort the entries, and turn them into useful strings to send out of Drafts.

Time Tracking Using Plain Text and Drafts - Part 2 (The Format)

At its most basic level, my plain text time tracking consists of a simple, intuitive, and flexible format for typing out how I spend my time on a given day. I make the process easier with Drafts actions, but they aren’t necessary.

Here’s an example of a plain text time slip:


2020-02-07 (Friday)

08:30 ABC Corp. - Review lease.

10:12 XYZ Co., motion to dismiss - Prepare for oral argument of upcoming motion to dismiss.

12:06 lunch

Had a great lunch at new sushi place.

13:00 ABC Corp. - Review and respond to addition email question from Mr. ABC.

13:30 Jones, meetings - Interview Ms. Jones regarding dispute with business partners.

14:48 Jones, strategy - Prepare memo outlining potential claims.

16:30 ABC Corp.

17:00 head home

Remember to buy wine on the way home!

This time slip has the advantage of being easily readable as is. The format has only two requirements:

  1. First, somewhere in the note (usually at the top), I need a line that starts with the date in ISO format, e.g. 2020-02-07 in the above.
  2. Second, the note includes a series of lines that start with a time, identify the client, and describe the work being done in the following format: HH:MM client name [, optional project name] - Description of work.. Everything except the time and the client is optional.

Any lines that do not start with HH:MM are ignored. That gives me the flexibility to add whatever additional whitespace or information I may want between time entries without mucking up the eventual calculation (like the notes about sushi and wine above) My only caveats: client name and project name (if any) can’t include a , or the - (space-hyphen-space) sequence, since those are used to identify the breaks between client, project, and description.

That’s all there is to it. It’s easy to remember, provides all the information usually required for billing, and is easily human readable.

To make it even easier to type this format I’ve taken two additional steps:

  1. Creating a button in Drafts that inserts the current time. It’s just easier to hit that button than to type out 14:32 or whatever the time happens to be. (As a bonus, you can optionally have the insertion button round the time to the nearest tenth or quarter of an hour, depending on you billing practices.) Here’s a very basic example.
  2. Creating an action to start a timeslip on a new day. This action creates a new draft, inserts the date (with the day of the week in parentheses), a couple of line returns, and the current time. I can run that action when I get to work in the morning (or even use a shortcut to prompt me to do so).

So far, so good. My next post will address how I use Drafts to manipulate the data in these time slips.

Time Tracking Using Plain Text and Drafts - Part 1 (Why)

As an attorney, I need to track my time. For the past several years, I’ve done all my time tracking using plain text and the app Drafts. Here’s why:

When I started practicing law, I tracked my time with pad and pencil. I noted the times I started and stopped work, and when I switched from one client or project to another. Simple. The resulting slip of paper was flexible and easy to understand. But it was not ideal. My note pad wasn’t always at hand. And I would still need to manually convert my note into time entries for my law firm’s billing software.

When I first got an iPhone, I tried various time tracking apps. These apps generally present the user with some pre-defined set of clients and projects, have timers that you tap to start and end, allow for entry of notes and, sometimes, give you useful export options. These apps were ok, as far as they went. But they had drawbacks. Setting up new clients or projects easy was involved. If you forgot to switch timers, adjusting the timer in the app was difficult or at least annoying. Export options, if any, were not as flexible as I’d like. And descriptive notes were usually added in a separate interface, several taps away from a daily overview. I wasn’t satisfied.

I’d been exploring using plain text for my personal notes. But it wasn’t until I learned about Drafts that using plain text for time tracking really started to make sense. Drafts had two key ingredients my other notes apps had lacked: The ability to add custom buttons to make typing text easy, and the ability to create custom actions to process that text and send it elsewhere.

I’ll explain how I use plain text and Drafts to track my time in the next few posts.

(Plain text legal on hold for a little bit due to paying legal work)

Post iPhone event feeling? Still not all that excited. On the upgrade program, so will be getting a new phone, but Midnight Green is meh and none of the 11 colors are compelling either. Perhaps it’s back to black for me.

Pre-iPhone-event feeling? Way less excited than normal—and not even all that excited for iOS 13 release given reported bugginess.

The other thing to keep in mind is that Markdown (including MultiMarkdown) allows you to include raw HTML elements. For example: If you need a line break inside a table cell, you could just type <br> to get what you need.

MultiMarkdown metadata is an interesting addition to a plain text legal workflow. It could allow you to more easily use templates, or specify the court for a pleading, resulting in appropriate formatting for the specific jurisdiction.

It’s feeling very much like fall tonight, even with the crickets.

Because MultiMarkdown is based on Markdown, which was designed to generate HTML, there is no easy way to apply page-based formatting. This presents a problem when courts require footers or line numbers. But LaTeX may provide an answer.

*Ironically, some of the most traditional caption blocks are relics of the plain text typewriter era, when the table boxes were literally drawn with parentheses, dashes, colons, or other characters.

A caption block in MultiMarkdown is challenging. Essentially, it’s just a two-cell table. But MMD needs HTML to allow line breaks within a cell. And MMD doesn’t have a mechanism for specifying which table borders are visible.

Legal citations are a major challenge in MultiMarkdown.

MultiMarkdown allows for citations, but it’s focused on academic citations, rather than legal citations. And it’s not clear whether these citations can be adapted for legal purposes.

In MultiMarkdown, the syntax for marking up citations is fairly straightforward: [citation text][#cited source nickname] followed by (somewhere) a [#cited source nickname]: Full Source Name. (You could also add the full source inline, but that can get unweildy).

The difference between academic and legal citations is important. In legal writing, the source is usually explicit and included right in the text itself. E.g., ABC Inc. v. XYZ, LLC, 123 F3d 456, 458 (13th Cir. 2025). And that case (or statute or rule) is listed in the Table of Authorities, with a reference indicating on which page or pages it appears.

Ideally, we’d like to mark that up as follows:

(in the table of authorities):

[#ABCvXTZ]: _ABC Inc. v. XYZ, LLC_, 123 F3d 456 (13th Cir. 2025)....[page number(s)]

(in the text):

[_ABC Inc. v. XYZ, LLC_, 123 F3d 456, 458 (13th Cir. 2025)][#ABCv.XYZ].

In short, a Table of Authorities is a one-to-many listing, where you start with the “one,” and use it to look up the “many.”

Academic citations are the opposite. While there is still only a single source listed in an academic bibliography, the text contains multiple citations. As you read a paper, you may want to tap on the citation to see the full source citation. But you don’t start with the bibiliography and work backwards. Academic bibliographies are a many-to-one listing, and it does not appear capable of generating links back to the places in the text that a given source is cited.

The academic equivalent of a table of authorities would be something more akin to an index than a bibiliography. But, alas, there is no “index” equivalent in MMD, (other than a table of contents for headings).

Still, at a meta-level, the MMD syntax for citations gives us nearly everything we would need to generate a table of authorities. So perhaps we can do some processing of a MMD file to get what we need.

MultiMarkdown footnotes work just fine.[^Though if you don’t do in-line footnotes, you may have to keep track of the names/numbers yourself.]

MultiMarkdown tables are pretty basic, so any styling you might need (eliminating some of the borders, etc.) will need to be added later.

MultiMarkdown headings for contracts can also be tricky because there is no direct way to indicate whether or not the headings should be numbered, or how that numbering should work. (1.1.4 v. I.A.1.a etc.)

MultiMarkdown signature blocks are not all that difficult. The hardest part is getting nice signature lines. Escaping underscores seems to be the easiest way, though getting the right number remains a challenge.

Numbered cross references in MultiMarkdown are tricky

In MMD you can make a cross-reference using [][name of section] where name of section is a section title (or a reference name specified in brackets), but there’s no good way to refer to the section number.

For example, in a document with a section called “Preamble” you can type:

"Member" has the meaning ascribed in the [preable][Preamble].

But you can’t type:

"Member" has the meaning ascribed in Section [number??][Members].

The number is uwknown until the MMD document is processed into another format, and MMD doesn’t provide any shorthand for that number—only a way to refer to the section name.

A script might be able to add those references in—manually inserting numbers by counting up by section—but that script is an extra moving piece that would have to be developed.

Getting a reference to the section number in LaTeX, however, is easy. So the challenge is how to get an intuitive reference to the section number in MMD that LaTeX will properly recognize.

Towards a legal markup language

Court-mandated styles may require adding:

  • line numbers (reset by page)
  • footers with document title and counsel information
  • caption blocks with court names, case numbers, titles, and party names).

Towards a legal markup language

What markup do we need for pleadings?

  • everything from contracts, plus:
  • footnotes
  • cites/table of authorities
  • a caption block

(We’ll deal with court-imposed formatting requirements separately.)

Towards a legal markup language

Starting simply: what markup do we need for contracts?

  • Sections, subsections, subsubsections
  • Cross references
  • Signature blocks
  • A table or two (maybe)

That’s really about it.

To take a step back, a plain text legal solution must:

  1. Use markup that gives easy access to most semantic elements a lawyer needs to write.
  2. Add a styling language or template that interprets 1 and adds any missing components.