Spreadsheet interface and template filling is done in Google Apps.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Documentation for Google Apps Codebase

How does the Legalese Google App work?

About this Document

This document describes the version 1.0 codebase.

HOWTO Set Up a Development Environment

To start hacking on the Google App internals, you need to set up a development environment.

Clone a Company Spreadsheet

First, clone the Legalese Demo Master if you haven’t already. (If you ran the tutorial, you should have cloned it.) To clone it, click on File / Make a Copy. Customize it with the name of some made up company.

We will call your copy of the Demo Master a Company Spreadsheet.

Clone the LegaleseMain library.

Open the LegaleseMain library. That’s the official production library. You don’t want to mess with that for development.

Go to File / Make a Copy. Now you have a Copy of LegaleseMain. That’s the version you will edit.

Close the tab for the original LegaleseMain library.

In your copy of LegaleseMain, go to File / Manage Versions. Give your version a name like “init”, then Save New Version.

Open File / Project Properties. Copy the “project key”. It should look something like M6YlbsVrWR18KeWvOMc3708UQWaHMB8in. If you are lazy you can try grabbing the document ID out of the URL bar but that’s not officially supported.

Point your cloned Company Spreadsheet at your cloned Copy of LegaleseMain.

Each Company Spreadsheet has an associated Google Apps Script, which you can get to from Tools / Script Editor.

That Company Script comes from code.js. It doesn’t do much; all the heavy lifting happens inside the LegaleseMain script.

Click on Libraries / Resources….

Remove the existing import of LegaleseMain.

Paste the project key into the Find a Library box at the bottom of the dialog. Press select.

Set the identifier to legaleseMain.

Turn on Development Mode.

Hit Save.

Try running the cloned Incorporation tab.

Go to your cloned Company Spreadsheet. Reload the browser tab, because the script changed.

Go to the Incorporation sheet. You can find it at the bottom row of tabs.

Click on Add-ons / Generate PDFs.

Google will ask you to authorize the app. Allow.

When the script finishes, an output link should appear in cell E6.

Open that link.

You should see a bunch of XMLs.

Yay! Your cloned Company Spreadsheet is working well against your cloned LegaleseMain library.

In a few minutes those XMLs should turn into PDFs.

It Didn’t Work.

This section will accumulate Tips & Tricks for handling common failure scenarios along the way to setting up a development environment.

Try Tweaking Your Cloned LegaleseMain library.

With your development copy of the LegaleseMain library open, go to the LegaleseMain.js tab.

In the vicinity of Line 88 you should see a commented out DoNothing.

Uncomment it. Reload your Company Spreadsheet.

If you see the new DoNothing, and calling that function works, then you are now able to make bigger edits! Yay!

HOWTO push a new version of the library to production

make sure it works in development

in your personal development environment you should be confident that your new library code works.

run the test suite on dev

in the glorious future we will have a test suite that gives us this confidence.

paste the new version of your library code into the production LegaleseMain.

If you do not have edit rights to this file, you may have to talk to Meng.

Under File / Manage Versions, snapshot a new version

give it a descriptive name.

run the test suite on production

is a test suite available? if not, the Glorious Future has not yet arrived.

if the test suite is not available, test manually using the tutorial.

test it by hand by running through the tutorial as a brand new user.

after cloning the Legalese Demo Master to run the tutorial, under Tools / Script Editor … Resources / Libraries, import the latest version of the tutorial.

(you might not need to do this, especially if Development Mode is turned on, but it doesn’t hurt.)

if the tutorial doesn’t work using the latest version of the script, go back and fix it in dev.

update the Demo Master’s library version

if the tutorial does work using the latest version of the script, then:

In the script for the production Legalese Demo Master, click on Resources / Libraries and update the library import to the latest version.

you’re done!

HOWTO debug your script

If you’re getting a red error message when you run a script, grab the logs before they disappear.

Open Tools / Script Editor.

In the script source interface, open View / Logs. Copy and paste those logs to a text file or email somewhere.

In the script source interface, open View / Execution Transcript. Copy and paste the execution transcript.

The logs are output by Logger.log calls. (In Legalese, xxLog wrappers are defined in each .js library; see the bottom of util.js.)

The execution transcript is output by the Google Apps Script system itself.

Usually, the cause of the red error message of death can be found at the bottom of either the logs or the execution transcript, because that’s where Google Apps Script gave up.

If you’re reporting a bug to a developer, using email or a Github issue, please include both the logs and the execution transcript as attachments in your bug report, and also include the last few lines of both in the body of your report.

My error is inside an XML template!

If the error message brings you to templates.js, to the line reading

var filledHTML = newTemplate.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).getContent();

Then the error lies inside an XML template instead of a .js library. In that case, see Google Apps on Debugging Templates, and good luck on your adventure.

The logs don’t show me enough!

Logs and execution transcripts get truncated; the buffer is only so big.

Go into util.js in your development version of LegaleseMain. You will find a myLogConfig object which determines the Log Filter Level for each module.

By default, the log filter level is 6. Any log lines with level 1, 2, 3, 4, 5, 6 will appear in the logs.

Inside your module, xxLog calls can pass an optional log level parameter. The default log level is 7.

Only unusual or important events are logged with a level below 7.

Routine events are logged with 8 or above.

If you want to debug a particular module, you probably want to view its log events, selectively and temporarily.

In util.js, change the log filter level for your desired module to 8. Don’t do this in the Git repository; do this in your running instance of LegaleseMain, inside the util.js module.

Reduce the log filter level of the other modules accordingly to mute them.

HOWTO write a new XML template

You find yourself inside a text editor. An XML template lies before you. On the ground around you are angle brackets, letters from A to Z, and special characters like ? and =, ready to be placed in the template.

A scroll appears. It reads: Your mission, should you choose to accept it…

You open the scroll. It continues: … produce an XML template which, when the user clicks “Generate PDFs”, turns into a PDF that people can sign.

A help desk appears. The sign above it says: “Useful Facilities for the Template Developer.” Good, you think. I should check this out.

Background and Terminology

Legalese’s v1 system helps investment deals get done.

In Legalese lingo, a deal represents a state change in the history of a company. Specifically, a change to the cap table.

What’s a cap table? The Capitalization Table records all the changes to the shareholding of the company. With each new round of investment, the cap table grows another column. With each new shareholder who joins the company’s “register of members”, with each new investor who subscribes for convertible debt, the cap table grows another row.

The first deal is usually Incorporation.

The second deal is usually Founder Vesting.

The third deal is usually Angel Round.

The fourth deal is usually Seed Round.

Legalese considers a corporation to be the sum of its contracts, notices, resolutions, and regulatory filings.

Legalese generates one or more of these kinds of documents per deal. After all the documents are signed, the deal is complete.

Legalese uses Google Spreadsheets to record these deals.

A Legalese Spreadsheet contains one sheet per deal.

A Legalese Spreadsheet also contains special sheets which do not correspond to deals; instead, they describe other aspects of the company.

Standard Templates

Legalese comes pre-loaded with many useful templates to help deals get done.

For example, we have templates for SAFEs, KISSes, and convertible notes.

Legalese also comes with utility templates, which get invoked as part of a bigger template. For example, we have a template that expands to a signature block. If your SAFE needs five people to sign it, the SAFE template will include the signature template five times, with different parameters each time. So you can think of a template as a function, with type parameters -> XML.

All of these templates are listed in the Demo Master, in the Available Templates tab. There is a tutorial on how to add your own template to the system.

This document explains what to put inside your template.

We assume you already have a working Legalese Spreadsheet

If you have run the tutorial, then you have already cloned the Legalese Demo Master. You should have renamed that spreadsheet after your company: in this document, we assume you have a company called Frog Porridge, so you renamed your spreadsheet Frog Porridge - Legalese.

Looking at the tabs at the bottom of the screen, you will notice a couple of special sheets, named Entities and Cap Table.

Special Sheet: Entities

Your corporation has relationships with many entities. It has directors. It has a corporate secretary. It may have employees, auditors, and creditors. It has shareholders, and it may have other kinds of investors who are not shareholders.

All of these entities are recorded in the Entities tab. You’ll notice that the Entities tab has a certain structure: the first few lines in the sheet are title and prelude; then a dark gray line begins with the section header ENTITIES. The rest of the line has column headings which define the attrbutes for each entity.

Keep in mind that other sections may obey different syntax conventions, so don’t expect every section to have column headings the way Entities does.

Section Definition: Entities

Column A of an entity row is the Core Relation. It’s not labeled as such in the section header line, because column A of the section header line is already taken up with the word ENTITIES. But trust me, it is the Core Relation.

An Entity’s Core Relation

Think of the Core Relation as the default role for an entity. An entity may take on other roles, depending on the deal.

The relation is, of course, with respect to the Company, which the principal entity. All the roles in Entities are with respect to the Company: for example, Founder.

Later you may find roles which are with respect to the company within the scope of a particular deal; for example, New Investor.

In the future we may get rid of this Core Relation concept, because it tends to collide with other roles.

For now, just keep in mind that if a Mister Moneybags puts money into the company, you should label them with the Investor Core Relation.

If you don’t know what Core Relation to use (and there are quite a few available, in the pop-up menu) just type in Other.

In the Entities section, do NOT set the Core Relation to:

  • New Investor
  • Old Investor
  • Shareholder

Those relations are reserved for system use. More about that later.

Essential Attributes

Each entity has the following essential attributes:

  • name
  • email
  • IDtype
  • ID
  • Address
  • State
  • Party Type
  • Short Name

Legalese’s standard templates assume that entities have these attributes. It’s OK to leave the attribute values blank – you can have blank cells – but if you delete the attributes themselves, bad things will happen.

The Address attribute is expected to be a multiline value. Use shift-enter or option-enter to insert a newline within the address field.

Additional Attributes

If, in your world, your entities need to have attributes beyond the standard ones, that’s fine; just add new attribute names in the section header line, directly following the other attribute names.

Don’t leave blank cells in the section header line between attribute names. I don’t know why I even have to say this.

Integration With Google Forms

I have a horror of humans typing in important data values, especially those not their own.

How many times has somebody emailed you their phone number, and how many times have you manually transcribed it instead of clicking copy-and-paste? Don’t bother denying it, I know you’re guilty. If it isn’t a phone number, it’s an email address. Or a name. And how many times have you typed it in wrongly? Copy-and-paste, people. Use it.

Google Spreadsheets integrates with Google Forms. And so does Legalese. If the end-user clicks Add-Ons / Legalese / Create form, Legalese will create a form that lets any entity submit attribute values. For instance, you could send out the Google Form URL to your investors, and ask them to fill in their address and other details. When they submit the form, the values automatically populate your Entities table. Don’t type things in by hand, people.

Configuring the Google Forms Integration

“When I created the form,” you say, “each attribute got its own form field, but it also got a bunch of extra metadata, like a helpful prompt and a field input type. Where did that metadata come from?”

You’ll notice a few hidden lines immediately above the ENTITIES section header. If you unhide those lines, you’ll see:


That’s where the metadata comes from. If you added new attributes of your own, and you want those new attributes to appear on the form, you’ll need to add the relevant bits of metadata above your new attributes, to match the metadata of the existing attributes.

The form attribute metadata is not documented any further at this time, because you’re smart and I’m sure you’ll figure it out.

Section Definition: Roles

Further down the page, you come to the Roles section. The syntax for the Roles section is NOT THE SAME as for the Entities section!

The point of this section is to say, hey, this entity that we already know about? Well, they’re not just an employee; they’re a shareholder too! This is how you tell Legalese that a given entity has multiple roles.

Every entity in a Roles section must be already defined in an Entities section elsewhere.

In column A: the role name.

In column B: the entity name. Or a [List Expansion]. If the value in column B is a role name in square brackets, then every entity with that role gets substituted in to have the new role.

For example, if column A says “Director” and column B says “[Founder]”, and if the Entities section defined three founders, then you’ve just told Legalese that every founder is also a director; all three founders now also have the role of Director.

Optionally, in columns C and D, you can have add new attribute name-and-value pairs to an existing entity. For example, if you want to record the company’s date of incorporation, you can set column A to be “Company”, column B to be “Frog Porridge Pte. Ltd.”, column C to be “Date of Incorporation:” and column D to be the actual date.

You can repeat the attribute name-and-value pairing in columns C/D, E/F, G/H, and so on. It is customary to right-align the name columns, and colorize them as well, to offer a bit more of a clue as to which is name and which is value.

Legalese is strongly typed! Use cell formatting sensibly!

Do please use Google Spreadsheet’s built-in cell formatting functionality to make the date an actual date type, not just a string containing a date. Most programming errors are type errors. This advice applies to all other spreadsheet fields, too: please, please don’t use a string when you mean to display a number or a currency.

Section Definition: Lookups

This is basically a scratchpad area for helper functions. Legalese ignores the content of a LOOKUPS section.

Special Sheet: Cap Table

Legalese sees your startup as a series of deals. Yeah, we know, there’s blood, sweat, and tears, and when you read The Hard Thing About Hard Things you see yourself in the starring role, and you may have lovingly chronicled the life of your startup in a thousand late-night tweets.

But Legalese is a FinTech/LegalTech app and we don’t care about any of that. All we care about is your cap table. “Show me the money!”

Each deal (usually) modifies the cap table in some way.

The Cap Table tab contains only one section: the CAP TABLE section.

The cap table displays all your deals on one sheet, and identifies the entities involved in each deal.

Column A of the Cap Table must be the exact name of a known entity. The best way to do this is have an = formula that links back to the Entities tab. Do NOT type the names in manually. Why not? Because if you find yourself updating a name on Entities, you need “John Smith” to become “Jonathan Smith” everywhere, at once, correctly. And if you type names in by hand, you lose referential integrity, and you will have weird errors, and you will blame Legalese, when actually it’s your fault, and it’ll all end in tears. Don’t let that happen. Don’t repeat yourself. Use an equals formula. Seriously.

The cap table sheet is an authoritative source of truth for Legalese. All kinds of things flow from the cap table sheet. It has to be correct.

I know, it’s tempting to run what-if scenarios in your cap table. Heck, everybody does it. You can do it too – as long as you do it in a duplicate tab. Don’t do it in the main cap table. Copy the tab and call it Hypothetical Cap Table. Knock yourself out. Just don’t mess with the real Cap Table.

How to add a new row to the cap table

How to fill values in to the cap table

How to add a new column to the cap table

Things Need To Add Up

the Total column should make sense.

Available Templates

Looking at the tabs at the bottom of the screen, you may notice another special sheet, called Available Templates.

The version of Available Templates in your cloned Frog Porridge spreadsheet doesn’t actually do anything. By default, every time the user runs Generate PDFs, the script goes off and reads the main Demo Master’s Available Templates sheet. Don’t bother changing anything in the local copy in your spreadsheet, because those changes won’t be picked up. It’s just there for reference.

So refer to it. You will see that each template has a handful of attributes:

  • name
  • title
  • URL
  • To
  • CC
  • Explode
  • Nocache
  • Requires

Other attributes like baseURL and urlPath are helper columns used to construct the URL, but they aren’t read by the Google Apps system.

Name, Title, and URL

Each template has a name – a short identifier, like “some_template_name”.

Each template has a title – this is used as the filename for the generated XML and PDFs, and may be visible when the template gets pushed out to the e-signature system.

Each template has a URL – this is how Google Apps knows where to find the source XML in the first place. Usually the template URLs point to Legalese’s Github repository. If you’re running your own templates, or doing a dev version, the URL may be rooted somewhere else.

To, CC, and Exploders

When a template gets filled, some people have to sign it, and some people have to see it.

Templates don’t identify those people individually. Rather, we refer to them by role.

If a document needs to be signed by all the directors, then under To, put director.

If a document needs to be CC’ed to the corporate secretary – who has to see it, but doesn’t have to sign it – then under CC, put corporate_secretary. This tells the e-signature backend what to do.

If a document only needs to be signed by one director, not all the directors, then under To, put director[0]. This is array notation.

If a document needs to be signed by multiple sets of roles, join those roles with commas. Don’t use a space.


When you use the To column, all the entities of all the roles get expanded inside the template, so you get a single document with multiple signature blocks.

What if you instead want multiple documents, each with a single signature block?

This might happen, for instance, if you are sending out a shareholder notice, requesting the courtesy of a signature; you want to send a separate message to each shareholder, rather than a single huge message to all the shareholders.

This is what the Explode column is for. Under the explode column you can only put one role; no commas. Every entity under that role will get their own copy of the document. Nobody sees anybody else’s communications, and you get more privacy.

Skip Exploding

Sometimes someone should be left out from the exploder list. If that’s true, in a ROLES section somewhere, set their entity attribute to legalese status: skip_exploder <some_template. You can also say skip_exploding. I could never keep them straight in my head, so both forms work.


By default, Legalese will cache for five minutes any XML templates it fetches over HTTP. This is most beneficial for templates which are included several times per deal, e.g. inc_party.

If you’re actively tweakign a template, turn off caching by setting nocache to TRUE.


Usually blank.

A template that defines a requires role will not be evaluated if that role is unpopulated by any entities.

Sections In The Deal Spreadsheet

We’ve talked about the two special tabs that describe the company as a whole: Entities and Cap Table.

Now it’s time to look at a deal, that generates some paperwork that materially changes the state vector of your corporation.

Go open up the Seed Round tab. In there, you’ll see sections named:

Key Terms

This is where you write down the details of the deal. In fancy lingo, you parameterize the deal.

Column A is the name of an attribute. For example, it might say Pre-Money Valuation.

Column B is the value of the attribute. For example, it might say S$2,250,000.

Column C and on are ignored.

Inside the data structure, which we will get to in a moment, the internal representation is a little different:

data.pre_money_valuation = "S$2,250,000";
data._orig_pre_money_valuation = 2250000;
data._format_pre_money_valuation = "[$S$]#,##0.000";

Every row in the TERMS section of a spreadsheet turns into an attribute in the data.* object in a template.

Note: we rely on Google Spreadsheet’s cell formats!

This is worth repeating, so I’ll say it again:


What does this mean? Well, take a look. Go into a cell that contains an amount of money. You’ll see that the raw numerical value appears, absent any currency symbols or commas. Yes, this is as it should be. No, a literal dollar sign is not part of the value of the cell. That dollar sign is part of the format of the cell.

Formats in a spreadsheet are like units in science, or types in programming.

What if you want to change the format of a cell? Click on the 123 icon and pick the appropriate representation. If you don’t like the number of digits after the decimal point, click on the .0 and .00 icons.

An enormous class of Legalese errors arise from when a cell that should contain a number actually contains a string. This will happen if you type in a number with commas. This will happen if you type in money with a dollar sign. You’re putting a square peg into a round hole. You’re committing what’s called a type error. To avoid type errors, don’t type in commas and dollar signs! Instead, change the cell format in the proper place.

Attribute Normalization

We convert attributes with names like Bob's Shack Address: to bobs_shack_address. See format.js, function asvar_.

Minor Terms

This section is treated exactly the same as Key Terms. In fact, Legalese doesn’t even bother to distinguish MINOR TERMS as a section header: that row just turns into the attribute data.minor_terms_specific_to. LOL.

Just because a row looks white-on-black doesn’t mean it’s special. We look not at the color of its skin, but at the content of its characters.


This is where things get technical. The config section tells Legalese how to go about turning spreadsheets into XML and XML into PDF. You’re a Legalese developer, so you’ll be spending a lot of time in the config section. (As opposed to your end-users, who will spend time in the TERMS and CAP TABLE sections.)

Attributes from the CONFIGURATION section turn up in the data._config object. Note the underscore.

Let’s get some easy pleasantries out of the way.

You might see a // cell. That explicitly means: ignore this cell, and everything to the right. This is usually a sign of a bunch of values used by helper functions.

If Column A contains the word ignore: then the spreadsheet parser ignores that row. Well, it doesn’t really; it’s treated like anything else. It’s up to you not to look at any config attributes called “ignore”.

You’ll observe that sometimes a row will have a blank Column A, but a nonblank Column B. That’s because of the ditto effect: a blank Column A is assumed to take its value from the row above. That allows us to effectively have hanging indentation, which looks better than repeating Column A row after row, don’t you think? In fact, the ditto rule may also work for columns B and onward, I’m not sure. Looking at the source code, I’m still not sure.

You have to be careful, though, because if you want to set just one ignore: in the middle of a few template: rows, it’s easy to accidentally not just that row but everything after it. To unignore the next line, explicitly say template:.

You’ll observe that sometimes Column C is treated as a comment. Sometimes Columns C and D are treated as key/value pairs. All this dynamism is for your benefit, because we know that sometimes the world can be complicated, and describing its complexity requires complex data structures. As a developer, inside the XML template, Legalese breaks it down for you a whole bunch of ways.

Suppose your config section has a row where Column A contains “A”, Column B contains “B”, and so on, yea, unto C, D, E, and F.

data._config.A.value = B
data._config.A.values = [B,C,D,E,F]
data._config.A.tree.B.C.D.E = F
data._config.A.dict.B = [C,D,E,F]
data._config.A.dict2.B.C = [D,E,F]

Which accessor you choose depends on what attribute you’re trying to access.

For example, to get a list of templates desired by the deal sheet, data._config.templates will contain an array of template names.

A template may define Column C = “style”, in which case D will be the name of a font.

Here’s how the inc_covertitle.xml helper template extracts that attribute:

var style = data._config.templates.tree[data._templateName] ? data._config.templates.tree[data._templateName].style : null;

I don’t think columns B and onward get normalized.

Configuration Items

Config attribute names are case-insensitive.


A list of template names. The names must correspond to the names from the Available Templates sheet.

To turn off a template, set the first column to ignore:. And remember to set subsequent columns back to Templates:.

Templates and Template are synonymous.

Add To Folder

The URL of a Google Drive parent folder where the output folders will go. Each time the end-user runs Generate PDFs, Legalese creates a new folder timestamped with the current time, and dumps output XMLs and PDFs into that folder.

It’s a good idea to organize those folders under a startup’s Corporate Documents folder. Of course your startup already has a Corporate Documents folder, right?


default: false.

Normally, Legalese deletes the XML file when it produces a PDF. Set this to TRUE if you want to see the XML and InDesign working files.

Omit Date

default: false.

Legalese usually stamps every document it generates with dates, on the cover, in the margins, and in the text. If you set this to true, Legalese will leave out dates from the documents it generates.

Show Investors On Cover

List all the names of the investors on the front page.

Echosign Expires

Tell the e-signature backend that the document will expire after a few days.


As one good turn deserves another, one Legalese sheet can include another.

The most obvious example is the automatic and invisible inclusion of an Available Templates sheet. If a deal sheet doesn’t itself define an Available Templates section, then Legalese will automatically default in all the templates from the Demo Master’s Available Templates sheet.

A deal sheet can also include a different deal sheet; in that case, the TERMS will be read in. This is not a common situation, but may occur if there is a main deal and multiple sub-deals.

There are a few complex rules around what happens in a chain of nested includes; the details are described in a comment at the top of readrows.js and we won’t go into them right now. Some other time we should probably move that documentation into this file.

You can have a ROLES section in a deal spreadsheet.

You’ll often need a way to say, for the purposes of this deal, Entity E has role R.

If Entity E always has role R, then you can put that relation directly into the Entities tab’s ROLES section.

But if Entity E only sometimes has role R, and it depends on what the deal is, then you should put that relation into a specific deal’s ROLES section.

If you look at the Demo Master’s ESOP and Vesting tab, you’ll see that a hidden row INCLUDE*s the Entities sheet, and then a *ROLES section adds some attributes to a few entities.

Accessing the Deal from inside the XML Template

So far we’ve described the Legalese Spreadsheet largely from the point of view of an end-user filling in the entities, cap table, and terms.

So. Now the the end-user has filled in the spreadsheet with all the details of their startup.

How does that information become available to you, the template author?

We assume basic familiarity with Google Apps Script’s HTML Template service. If you don’t know what <?= ?> means, get a quick refresher.

In a deal sheet, under CONFIGURATION, you specify one or more templates. When the user hits “Generate PDFs”, Legalese retrieves the templates, sets up a few objects – most importantly the data object – and evaluates each template, with those objects passed in to the template.

The Data Structure

Following the “Pushing variables to templates” example, most of the spreadsheet state, suitably digested, is available in the data.* object.


The Legalese spreadsheet defines entities and roles generally.

In the To and Exploder columns of the the Available Templates sheet, you’ll see specific roles identified for expansion into parties.

What’s the difference between roles and parties? Roles are just kind of hanging out on the bench: wide receivers, linebackers, safeties. A template beckons at some of those roles to come on to the field. Once they’re in the game, running a specific play, they are parties, to a specific contract.

A deal may be made up of multiple contracts, and each contract may specify a different set of parties.

Inside a template, you’ll find a data.parties object, which contains all the parties that were identified for that template.

data.parties looks like this:

  "company": [
    { // let's call this data structure a Party Structure
       "_origin_spreadsheet_id": ...,
       "_origin_sheet_id": ...,
       "_spreadsheet_row": ...,

       "name": ...,           "_format_name": ...,           "_orig_name": ...,           "_name_firstline": ...,
       "email": ...,          "_format_email": ...,          "_orig_email": ...,          "_email_firstline": ...,
       "idtype": ...,         "_format_idtype": ...,         "_orig_idtype": ...,         "_idtype_firstline": ...,
       "id": ...,             "_format_id": ...,             "_orig_id": ...,             "_id_firstline": ...,
       "address": ...,        "_format_address": ...,        "_orig_address": ...,        "_address_firstline": ...,
       "state": ...,          "_format_state": ...,          "_orig_state": ...,          "_state_firstline": ...,
       "party_type": ...,     "_format_party_type": ...,     "_orig_party_type": ...,     "_party_type_firstline": ...,
       "short_name": ...,     "_format_short_name": ...,     "_orig_short_name": ...,     "_short_name_firstline": ...,
       "description": ...,    "_format_description": ...,    "_orig_description": ...,    "_description_firstline": ...,
       "bank_details": ...,   "_format_bank_details": ...,   "_orig_bank_details": ...,   "_bank_details_firstline": ...,
       "signatory_name": ..., "_format_signatory_name": ..., "_orig_signatory_name": ..., "_signatory_name_firstline": ...,
       "roles": {
                "company": [ "Name 1" ],
                "founder": [ "Name 2", "Name 3", ... ],
                "employee": [ ... ],
                "investor": [ ... ],
                "esop": [ ... ],
                "corporate_secretary": [ ... ],
                "state_regulator": [ ... ],
                "bank": [ ... ],
                "director": [ ... ],
                "accountant": [ ... ],
                "new_investor": [ ... ],
                "brand_new_investor": [ ... ],
                "shareholder": [ ... ],
                "voting_shareholder": [ ... ],
                "nonvoting_shareholder": [ ... ],
            "_unmailed": true
    "founder": [ 
        { // Party Structure, as above, but with an interesting twist:
            "_role": {
                "Name 1": [
            "_es_num": null,
            "_to_email": ...,
        { // Party Structure for second founder
    // and other explicit roles identified in the Entities tab and in a ROLES section

Note that data.parties.company[0].roles offers a list of all the roles, including imputed roles, relevant to the current round.

Note that data.parties.<EVERYBODY-ELSE>[N]._role offers a list of all the roles vis-a-vis the company, including roles imputed for the current round.

As template developer, you are responsible for making sure that a template’s To column, in its Available Templates definition, corresponds to the use of data.parties inside the template’s XML.

Technical Nit: Legalese also generates notices and resolutions, which are not exactly contracts, but are similar to contracts, in that they require signatures. Can a document have only one kind of role? Parties with no counterparties? Sure.

Arguably we should move this out into a top-level parties object.


We talked about this above. Now we dive into detail.

Arguably we should move this out into a top-level config object.

You can see how the values are arranged in different ways. Which is most convenient for you to access?

If a config attribute is a simple key/value pair, go for .value.

    "add_to_folder": {
        "asRange": {},
        "values": [
        "dict": {},
        "tree": {
            "https://drive.google.com/drive/folders/...": null
        "value": "https://drive.google.com/drive/folders/..."

To pull the list of templates, do Object.keys(data._config.templates.tree).

A given template (in this case the debug template) may have further attributes of its own: data._config.templates.dict2.debug.style is ~”sketchRockwell”~. That’s the name of a font, by the way.

"templates" : {
   "dict" : {
      "debug" : []
   "values" : [],
   "tree" : {
      "preemptive_waiver" : null,
      "debug" : {
         "style" : "sketchRockwell"
      "dev_mr_issue_shares" : null
   "asRange" : {},
   "dict2" : {
      "debug" : {
         "style" : [

Some dictionaries don’t run that deep. To get the echosign title value, pull data._config.echosign.dict.title.

    "echosign": {
        "asRange": {},
        "values": [
            "Seed Round Investment"
        "dict": {
            "message": [
                "For review and signature."
            "title": [
                "Seed Round Investment"
        "tree": {
            "message": "For review and signature.",
            "title": "Seed Round Investment"
        "value": "title"

Generally a template developer should have little need to mess about with the data._config object inside a template.


If your template was defined with Explode : director then you can expect to see data.director available. They’re always singletons so this is syntactic sugar for data.parties.director[0].


data.company is always available.

On the theory that a deal is always about your corporation, data.company is a syntactic-sugar alias to data.parties.company[0].


Everything in the TERMS section shows up as a direct attribute of the data object.

data.pre_money_valuation: JSON: "S$3,000,000"
data._orig_pre_money_valuation: JSON: 3000000
data._format_pre_money_valuation: JSON: "[$S$]#,##0"

data.amount_raising: JSON: "S$250,000"
data._orig_amount_raising: JSON: 250000
data._format_amount_raising: JSON: "[$S$]#,##0"

data.security_type: JSON: "SAFE"
data._orig_security_type: JSON: "SAFE"
data._format_security_type: JSON: ""

There are a ton of terms so we’re not going to display all of them.

some special or default values

These all have their place and purpose … to be documented later.

  • data._todays_date: JSON: “2 October 2016”
  • data._todays_date_wdmy: JSON: “Sunday 2 October 2016”
  • data.xml_declaration: JSON: “<?xml version="1.0" encoding="UTF-8" standalone="yes"?>”
  • data.whitespace_handling_use_tags: JSON: “<?whitespace-handling use-tags?>”
  • data.whitespace_handling_use_characters: JSON: “<?whitespace-handling use-characters?>”
  • data._timezone: JSON: “Asia/Singapore”
  • data._entitiesByName: (skipping value)
  • data._origparties: (skipping value)
  • data._templateName: JSON: “debug”
  • data.xmlRoot: JSON: undefined
  • data._include: JSON: undefined
  • data._include2: JSON: undefined

Other top-level variables

Besides data.* several other objects are available.


see How does a spreadsheet resolve its signatories?

Roles Imputed from the Cap Table

Suppose you’re working a deal that’s five rounds into the history of the company.

There might be dozens of entities. How do you cope with the question of which entity has which role?

For the sake of this example, let’s say that in the company’s rounds,

  1. the company has formed (Incorporation)
  2. the founders have set up a vesting plan for themselves (ESOP and Vesting)
  3. a bunch of friends, family, and fools have bought non-voting shares (FFF Round)
  4. a bunch of angels have purchased convertible notes (Angel Round)
  5. a seed VC fund now wants to purchase a SAFE. (Seed Round)

Note that in our Demo Master there is no FFF Round, but let’s pretend.

To make this deal happen, you’re probably going to want an easy way to:

  • display the before and after cap table for the company
  • identify all the voting shareholders
  • identify all the nonvoting shareholders
  • identify all the equity holders
  • identify all the debt holders
  • identify all the investors who hold preemptive rights
  • identify all the investors who hold ratchet rights
  • which investors have been in prevous rounds?
  • which investors are in the current round?
  • which investors are coming up in a future round (this hasn’t happened yet, but we’re including it for completeness; one could easily imagine evaluating an old round’s securities in the context of a new round that has been fixed, or a possible new round being assessed as a scenario.)

For a given pair of (shareholder, round):

  • what securities do they currently hold?
  • and what are the details of those securities?
  • what is their history of holding their securities? (in other words, in which round did they acquire the securities, and how much did they pay?)
  • for each security held prior to the current round, what interaction arises with the current round
    • in terms of preemptive rights
    • in terms of EGM approval resolutions as a member
    • in terms of required notices
    • in terms of conversion
  • some classes of shares, like ESOP shares, may be dynamically calculated
    • at a given time, how many shares are restricted, and how many shares are unrestricted?

These are all questions about roles.

Legalese makes it easy for you to do all these things, and more. If you’re here looking desperately for a solution because you’re doing this for reals, you can skip to the answers in The Captable Cookbook.

Or you can understand the theory, with a couple of case studies.

Case Study: Preemptive Notices

One of the most important parts of the deal has to do with existing shareholders’ pre-emptive rights. Before the company can offer shares (or securities that are convertible to shares) to new investors, it must first offer existing shareholders the opportunity to purchase those shares. That’s one of the things that makes a private company a closely held entity: if existing members buy all the shares the company has to offer, then no new shareholders will be privy to the company’s affairs.

How does this rule go into action? When the company decides it wants to issue new securities, all shareholders who are eligible to purchase those securities need to receive formal notice of the offer. In Legalese, that means we need a template that’s signed by a corporate representative – typically a director – that then explodes out to existing shareholders; and each existing shareholder can respond with a signature.

That template is called the preemptive_waiver. Why waiver? Because most of the existing shareholders respond with a standard answer: they waive their preemptive rights and let the new investor purchase the shares. If any of the existing shareholders decide to participate in the round, they don’t need to send back the waiver – the fact of their subscription for the current securities is prima facie evidence that they received the preemptive notice and are responding positively.

In the Available Templates sheet, the preemptive_waiver template has To = company and Explode = sitout_shareholder. That means that the preemptive waiver goes to any shareholders who are sitting out of the current round.

The cap table contains all the information needed to compute the sitout_shareholder role. Any entity which was previously a shareholder in the company, but is not re-upping – not participating in the current round – is a sitout_shareholder. That’s a good example of an imputed role.

An imputed role is a role which Legalese computed on the fly, as opposed to an explicit role. We could have called it a computed role but since we’re a LegalTech application we use the fancier, more legal-sounding word.

If you explicitly labeled an entity with a role, maybe back in the ENTITIES section of the Entities tab, or perhaps in a ROLES section (either in the Entities tab or in your current deal sheet), then that entity has an explicit role.

A Director role is a good example of an explicit role: the only way Legalese can know who a director is, is if you tell it.

But investor-type roles can be deduced from the cap table, and are prime candidates for imputed roles.

Who should receive (and who should respond to) preemptive notices?

In the preemptive notices, entitlements are described: “You own X% of the company, and we are raising $Y. If you want to partake, let’s suppose you have $Z burning a hole in your pocket. Now, you are entitled to put in $E, so if $Z < $E then no problem. If $Z == $E, perfect. But if $Z > $E, then we’ll have to wait and see what the other investors do.”

How are these numbers filled in? We explore that question in the next section.

How are preemptive entitlements calculated?

Let’s break down that sentence into multiple steps.

You own X% of the company.

What are the numerator and denominators of the fraction X?

The denominator is the fully-diluted shareholding of the company, which is to say the sum of:

  • class F unrestricted, as at the time of the expected closing
  • preference shares which hold voting rights, counted on an as-if-converted basis, which means we need to evaluate a conversion scenario
  • ordinary shares that have been issued and fully paid
  • ordinary shares that will be issued prior to the round, due to previously agreed conversion of debt or convertible shares (both fully paid)
  • ordinary shares that would exist if, prior to the round, warrants that carry the right of such notices, and could be exercised, were exercised

However, for purposes of computing the entitlements for the round, fully-diluted capitalization does not include the conversion of notes that specify that they convert after the round.

The numerator is the same as the above calculation, but filtered for securities owned by you.

We are raising $Y.

The company pulls a number out of a hat which is either equal to, or slightly larger than, the amount of money they actually plan to raise.

In Legalese, this is the Amount Raising term.

If you want to partake, let’s suppose you have $Z burning a hole in your pocket.

When something in a shop window catches my fancy, before I go in, I stop and ask myself: “what would I be willing to pay for that?”

I set my reserve price before walking in the shop, because I know that my easily fooled brain will, left to its own devices, anchor to the price tag and start rationalizing in favour of buying it. (Afterwards, of course, I will have buyer’s remorese. (If it is returnable.))

I know, I know, the reserve price that I set is still influenced by the totally irrelevant numbers I was previously exposed to, like the price of the Tesla at the showroom next door. But trying to set a price first has got to be better than just going in defenseless before the full might of the machinery of commerce.

Similarly, when somebody decides whether to buy a stock, they should first make up their mind whether the stock is undervalued or overvalued; they should decide how diversified their portfolio is, and how their exposure to various asset classes would be affected by a given trade.

So, the ideal investor would decide “I want $100,000 more of this stock”. If the preemptive notice says, “we are raising $10,000, of which you are ratably entitled to buy another $320,” the ideal investor would say, “i’ll take all $10,000”.

Or the ideal investor would say, “this stock is a dud. I want out.” And they would decide not to join the round no matter what.

Do most investors actually do that? No. They say, “okay, I don’t have as much information about this stock as I would like, so I’m going to go with the status quo fallacy and neither increase nor decrease my proportional holding in the stock.” So they check the box that says, “I will stay in for $320.”

Now, you are entitled to put in $E.

Anyway, that thinking is not our problem. Our problem is big enough: compute Y/X = E and tell the investor that $320 would maintain their pro-rata.

They can put in exactly that, or more or less than that. It’s just a number.

They give these indications in their response to the preemptive notice.

Some investors will say, “I want to put $640 in. I might only be entitled to $320. Find a way.”

Other investors will say, “I only have $160. What can I buy for that?”

Most investor will either say, “I’m tapped out” or “OK, $320 sounds good.”

Then it’s up to us to figure out what to do with their responses.

If $Z < $E then no problem.
If $Z == $E, perfect.
But if $Z > $E, then we’ll have to wait and see what the other investors do.

Share offers are computed over multiple iterations. You may have come across the notion of “Excess Rights” – that’s the second round runoff after the first round wasn’t fully subscribed by existing investors, but some existing investors would like to take more than their entitlement.

This blog post should give you the flavour of it, though not every aspect of that case translates to our situation.

In a runoff iteration, it is possible that part of the pro-rata entitlements came from convertible instruments which could convert. But in the first iteration, it is possible that the company ascertains, through dialogue with the investor, that the investor declines to convert. In that case, the convertible instrument no longer becomes part of the pro rata denominator. The takeaway: ratios might change from iteration to iteration.

We could do a case study here of an ESOP

Case Study: Who should sign members’ resolutions?

Easy: every entity who, prior to the current round, holds any equity security which carries voting rights, either in general, or with respect to the members’ resolutions in question.

Because sometimes a shareholder may hold a class of stock which is generally nonvoting – it only votes on certain resolutions that affect that class of stock. So, the “voting” characteristic of a shareholder is not so simple that it can just be a boolean that you set-and-forget; it is a function that returns a boolean; and the input to that function includes the resolution.

We probably need to expand the range of key/value pairs defined in a deal sheet to provide sufficient information to completely specify the security. This has been recorded as issue 230.

Definitions of Imputed Roles

If an investor is showing up for the first time in the current round, then they’re a brand_new_investor.

If an investor is participating in the current round, then they’re a new_investor, whether or not we’ve seen them before.

If an investor participated in a previous round, then they’re an old_investor, whether or not they’re re-upping in the current round.

If an old investor is not participating in the current round, then they’re a sitout_shareholder.

If an investor (prior to the current round) owns voting shares, then they’re a voting_shareholder.

If an investor (prior to the current round) owns shares, and all the shares they own are nonvoting, then they’re a nonvoting_shareholder.

If an investor (prior to the current round) owns securities that are not shares, then they are neither a voting nor a nonvoting shareholder; they are a nonshare_investor.

Sorry for the confusing nomenclature; like the recurrent laryngeal nerve, it is evidence that Legalese is not necessarily the product of intelligent design.

You do not need to explicitly define any of the above roles. They get computed by Legalese, and imputed to the relevant entities in the relevant round. Like any explicitly defined role, they may be used in Available Templates and in your templates. They appear in the data.parties object, and under each entity’s _role attribute.

What does shareholder mean? I don’t know. I don’t think it’s imputed. It should be; it should describe anyone who owns shares in the company, and who appears on the register of members – as opposed to an investor who owns debt. Debtholders don’t have to attend annual general meetings and don’t have to sign members’ resolutions.

The capTable and Round Objects

We’ve talked about roles. Roles are characteristic attributes of an entity: in relation to a company, at a given round, is someone an investor, a founder, a corporate secretary? In SQL, you might phrase the question: SELECT * FROM entity JOIN role JOIN entityrole WHERE entityrole.entity = entity.id AND entityrole.role = role.id.

But roles aren’t the only thing Legalese can help you with.

When you’re dealing with a specific investment round, you often need quantitative values: how many shares does somebody own? What kinds of shares? Are they voting or nonvoting? What about a mix? If they have a convertible note instead of shares, what are the terms of the note? How does the note convert? Is there a cap? A discount? A fixed price per share? Preferred stock has other attributes as well: what’s the liquidation preference? If we’re using a class of shares to represent a vesting plan, what does the vesting schedule look like? At any given point in time how many shares have vested and how many remain restricted?

In the deal sheets, end-users provide enough information for Legalese to compute the answers to these questions. You access those answers using two objects: the data.capTable object and the Round object.

Using the capTable object

Here’s a typical use of the capTable and Round objects:

Immediately prior to the Initial Closing, the fully diluted capital of the Company will consist of ordinary shares, Class F Redeemable Convertible Preference Shares both issued and reserved, and YC-AA Preferred Shares. These shares shall have the rights, preferences, privileges and restrictions set forth in .

Constructing the capTable object

A little internals documentation. The format of a Cap Table is different enough to every other sheet that it has its own parser. The parser breaks the cap table down into an array of Rounds. Because rounds build on one another, in sequence, the parser maintains state, such as a running total of different numbers of shares, from one round to the next.

The data.capTable Object: Data Structure

Arguably these things should be accessble from a top-level captable object.

Most of the meat of the capTable object is in its .rounds array.

    "termsheet": {},
    "captablesheet": {},
    "isValid": true,
    "activeRound": "Seed Round",
    "rounds": [
            "name": "Incorporation",
            "new_investors": {
                "A": {
                    "money": "S$13,500",
                    "_orig_money": 13500,
                    "_format_money": "[$S$]#,##0",
                    "shares": "13,500",
                    "_orig_shares": 13500,
                    "_format_shares": "#,##0",
                    "percentage": "90%",
                    "_orig_percentage": 0.9,
                    "_format_percentage": "0%"
                "B": {
                    "money": "S$1,500",
                    "_orig_money": 1500,
                    "_format_money": "[$S$]#,##0",
                    "shares": "1,500",
                    "_orig_shares": 1500,
                    "_format_shares": "#,##0",
                    "percentage": "10%",
                    "_orig_percentage": 0.1,
                    "_format_percentage": "0%"
            "ordered_investors": [
            "sheet": "__cycle__object[sheet]",
            "captable": "__cycle__object[captable]",
            "security_type": "Ordinary Shares",
            "approximate_date": "2015-06-30T16:00:00.000Z",
            "price_per_share": {
                "shares": "$1.00",
                "_orig_shares": 1,
                "_format_shares": "\"$\"#,##0.00"
            "amount_raised": {
                "money": "$15,000",
                "_orig_money": 15000,
                "_format_money": "\"$\"#,##0",
                "shares": "15,000",
                "_orig_shares": 15000,
                "_format_shares": "#,##0"
            "post": {
                "money": "$15,000",
                "_orig_money": 15000,
                "_format_money": "\"$\"#,##0",
                "shares": "15,000",
                "_orig_shares": 15000,
                "_format_shares": "#,##0"
            "old_investors": {},
            "shares_pre": 0,
            "by_security_type": {
                "Ordinary Shares": {
                    "TOTAL": 15000,
                    "A": 13500,
                    "B": 1500
            "brand_new_investors": {
                "A": "__cycle__object[A]",
                "B": "__cycle__object[B]"
            "name": "Seed Round",
            "new_investors": {
                "C": {
                    "money": "S$125,000",
                    "_orig_money": 125000,
                    "_format_money": "[$S$]#,##0",
                    "shares": "781",
                    "_orig_shares": 781.25,
                    "_format_shares": "#,##0",
                    "percentage": "3.846%",
                    "_orig_percentage": 0.038461538461538464,
                    "_format_percentage": "0.000%"

The data.capTable Object: Methods


[ { name: investorName, rounds: { name:, shares:, money:, percentage: } } ]


helper function


The capTableSheet_() object

contains utility functions for manipulating the Cap Table sheet.

Notethat there is some duplication with the capTable object, e.g. rewire.


other functions defined in captable.js

I’m not sure if these are still in use, or if they have been superseded by the capTableSheet methods.


The Holder object

a utility object type, found in Round, representing a specific entity’s holdings in that round, which is to say the two cells money and shares in the cap table’s major column.

Attribute Keys:


The percentage attribute is sometimes there, sometimes it isn’t; it is best not to rely on it.

Round.getOldInvestors() returns a dict of Holders.

Round.old_investors is a dict of Holders.

Round.new_investors is a dict of objects, keyed by name, but should in future become a dict of Holders.

Round.ordered_investors is a dict of objects, keyed by name, but should in future become a dict of Holders.

Holder Methods

returns the number of shares which are eligible for pro-rata calculations. This number is used in the preemptive_waiver template, as the numerator of a fraction.


returns the Entity object constructed by readrows from the Entities tab and ROLES sections of the current sheet.

This has different attributes than the Holder.

Why? Because the Holder is intended to represent the entity at a point in time – at the time of the current round, based on information provided in the captable, and related automated computations – while the Entity’s attributes are supposed to be true across multiple rounds.

Of course, ROLES attributes tend to be scoped to the current round, so this abstraction does not hold very well.

The “TOTAL” Holder

Captable maintains running totals by share type, using a special Holder named “TOTAL”.

The Round object: Data Structure

A Round object has the following attributes:

string, name of round as given in the cap table. should exactly match the tab title of the corresponding deal term sheet.
a dict of Holders, keyed by name
a dict of Holders, keyed by name
a list of names ordered by appearance in the cap table, from top to bottom
  • sheet
  • captable
2-level object keyed by security type name from the captable, and by investorName

The Round object: Methods

var activeRound = data.captable.getActiveRound();
var newIssues = data.capTable.getActiveRound().getNewIssues();
var oldInvestors  = data.capTable.getActiveRound().getOldInvestors();
var previousRound = data.capTable.getActiveRound().getPreviousRound();
Round = data.capTable.getActiveRound()

primary accessor; retrieves the round corresponding to the current deal sheet

Round = data.capTable.Round()

alternative accessor; retrieves the round with the given name


returns the Round object for the prior round, or undefined if there was no prior round.


returns the Google Apps Spreadsheet Sheet object for the given round


mutator that resets some of the formulas in the cap table sheetn, linking between major columns.

This function is useful because when end-users manually edit the cap table sheet they might move columns around or add rows, breaking the formulas; this function resets the formulas back to a good state.


returns the total number of shares eligible for pro-rata calculations.


returns the readrows object for the deal term sheet for the given round

The Entity Object: Data Structure

This is what data.parties contains.

The Captable Cookbook, with a side of ESOPs

Whew. That was a lot of theory. But sometimes you just have a simple question and want a quick answer – a little something suitable for copy-and-pasting.

This section shows Javascript. In actual use, this Javascript would be embedded within an XML template. The code snippets below use the notional output?= and output?!= functions to stand in for <?= ?> and <?!= ?> in the actual template.

If you really must copy and paste (I’m looking at you, brogrammers), go to the “See this in action” templates to see how they’re used in anger.

outline the before cap table

Don’t enumerate the investors, but just summarize the number of shares of different classes:

See this in action: AA-SG-SPA

<numbered_3_para>Immediately prior to the Initial Closing, the fully diluted capital of the Company will consist of <?= digitCommas_(data.capTable.getActiveRound().by_security_type["Ordinary Shares"].TOTAL) ?> ordinary shares, <?= digitCommas_(data.capTable.getActiveRound().by_security_type["Class F Shares"].TOTAL) ?> Class F Redeemable Convertible Preference Shares both issued and reserved, and <?= digitCommas_(data.capTable.getActiveRound().by_security_type["Series YC-AA Shares"] ? data.capTable.getActiveRound().by_security_type["Series YC-AA Shares"].TOTAL : 0) ?> YC-AA Preferred Shares. These shares shall have the rights, preferences, privileges and restrictions set forth in <xref to="articles_of_association" />.</numbered_3_para>

Takeaway: a data.capTable.getActiveRound() object contains a by_security_type object, which is keyed by security type, and has a special sub-key called “TOTAL”, which is dynamically maintained by Legalese as a running total.

display the before cap table

Who was an investor prior to the current deal? For the purposes of the cap table, both shareholders and noteholders are considered investors, as are any participants in the ESOP plan.

See this in action: dora

 var oldInvestors  = data.capTable.getActiveRound().getOldInvestors();
 var previousRound = data.capTable.getActiveRound().getPreviousRound();

 for (var hh in oldInvestors.holders) {
	with(data._entitiesByName[hh]) {
	  output?= ( hh );
	  output?= ( address );
	  output?!=( data.capTable.investorHoldingsInRound(hh, previousRound) );
The deprecated approach!

See this in action: ordinary_share_subscription_agreement pulls its list of shareholders from a different source:

 for (var i = 0; i < data.parties.shareholder.length; i++) {
	var myShareholder = data.parties.shareholder[i];
	if (! data.capTable.investorHoldingsInRound(myShareholder.name)) { continue }
	output?= ( newlinesToCommas(myShareholder.name) )
	output   ( " holds " )
	output?!=( data.capTable.investorHoldingsInRound(myShareholder.name) )

The latter approach gets its data from data.parties.shareholder; it does consult the capTable object because someone may have been an investor in the past, and then gotten out, so they shouldn’t be listed. And it uses investorHoldingsInRound to display the values.

Why do both approaches work? Because if someone’s under data.parties.shareholder, that means they aren’t in data.parties.new_investor, which means their investment didn’t change between the previous round and the current round.

Which approach is better?

If you’re writing new code, use the former approach. But know how to read the latter.

display the new investors in the current round

Who put money in, as part of this deal?

See this in action: dora

 var newIssues = data.capTable.getActiveRound().getNewIssues();

 for (var hh in newIssues.holders) {
	with(data._entitiesByName[hh]) {
	  output?= ( hh );
	  output?= ( address );
	  output?!=( data.capTable.getActiveRound().new_investors[hh]._inWords );

display the after cap table for the company

See this in action: AA-SG-SPA.xml

 var activeRound = data.capTable.getActiveRound();

 for (var hh in activeRound.ordered_investors) {
	with(data._entitiesByName[hh]) {
	  output?= ( hh );
	  output?= ( address );
	  output?!=( data.capTable.investorHoldingsInRound(hh) );

identify all the voting shareholders

identify all the nonvoting shareholders

identify all the equity holders

identify all the debt holders

identify all the investors who hold preemptive rights

identify all the investors who hold ratchet rights

which investors have been in prevous rounds?

which investors are in the current round?

which investors are coming up in a future round (this hasn’t happened yet, but we’re including it for completeness; one could easily imagine evaluating an old round’s securities in the context of a new round that has been fixed, or a possible new round being assessed as a scenario.)

compute the preemptive entitlement for a given shareholder

Go look at the source of the preemptive_waiver template:

Given that you presently hold <?= data.capTable.getActiveRound().sitout_shareholders[data.sitout_shareholder.name].shares ?> out of <?= data.capTable.getActiveRound().pre_money.shares ?> existing shares, you have the right to subscribe for up to <?= pro_rata ?>% of this new issue: <?

That needs more thinking through to match the logic defined in the case study above.

data.capTable.getActiveRound().pre_money.shares is obviously too general.

It should be:

  • class F unrestricted, as at the time of the expected closing
  • preference shares which hold voting rights, counted on an as-if-converted basis, which means we need to evaluate a conversion scenario
  • ordinary shares that have been issued and fully paid
  • ordinary shares that will be issued prior to the round, due to previously agreed conversion of debt or convertible shares (both fully paid)
  • ordinary shares that would exist if, prior to the round, warrants that carry the right of such notices, and could be exercised, were exercised

How do we get that? We have to compute a sort of instantaneous as-if-converted ordinary-shares equivalent.

class F unrestricted, as at the time of the expected closing

the formula is:

import qualified Data.Time          as Time
import qualified Data.Time.Calendar as Calendar

data Period = Daily
            | Monthly
            | Quarterly
            | Yearly
            | NDays Int

ymd = Calendar.fromGregorian

type Date = Calendar.Day

data PeriodBC = Business | Calendar

type UnrestrictedCount = Int
type RestrictedCount = Int

type URpair = (UnrestrictedCount, RestrictedCount)

newtype VestingParams = VestingSpec { effectiveDate :: Date
                                    , unrestricted  :: Integer
                                    , restricted    :: Integer
                                    , cliffDate     :: Date
                                    , periodLength  :: Period
                                    , periodVestNum :: Int

eoPeriod :: Period -> PeriodBC -> Date -> Date

pairOnDate :: VestingParams -> Date -> URpair

pair2restricted   :: URpair -> RestrictedCount
pair2restricted = snd

pair2unrestricted :: URpair -> UnrestrictedCount
pair2unrestricted = fst
data Period = Daily
            | Monthly
            | Quarterly
            | Yearly
            | NDays Int

data PeriodBC = Business | Calendar

type UnrestrictedCount = Int
type RestrictedCount = Int

type URpair = (UnrestrictedCount, RestrictedCount)

data VestingParams = VestingSpec { EffectiveDate :: Date
                                 , Unrestricted  :: Integer
                                 , Restricted    :: Integer
                                 , CliffDate     :: Date
                                 , PeriodLength  :: Period
                                 , PeriodVestNum :: Int

eoPeriod :: Period -> PeriodBC -> Date -> Date

pairOnDate :: VestingParams -> Date -> URpair

pair2restricted   :: URpair -> RestrictedCount
pair2restricted = snd

pair2unrestricted :: URpair -> UnrestrictedCount
pair2unrestricted = fst

unrestricted shares granted outright at time of issue plus min (the total number of restricted shares, (cliff shares, if the current date is after the cliff plus the PeriodVestNum times the number of periods that have occurred after the cliff ) )


This logic for now lives in the spreadsheet for two reasons. The spreadsheet has the date calculation machinery already built in, and also the spreadsheet itself needs to know the outcome of the calculations from round to round.

preference shares which hold voting rights, counted on an as-if-converted basis, which means we need to evaluate a conversion scenario
ordinary shares that have been issued and fully paid
ordinary shares that will be issued prior to the round, due to previously agreed conversion of debt or convertible shares (both fully paid)
ordinary shares that would exist if, prior to the round, warrants that carry the right of such notices, and could be exercised, were exercised
data.capTable.getActiveRound().ESOP.dynamicTotal(subclass: "F unrestricted",
												   atTime: data.capTable.getActiveRound().approximate_date);
<? var activeRound = data.capTable.getActiveRound(); ?>
<? var newIssues = data.capTable.getActiveRound().getNewIssues(); ?>
<para_1><b>Issuance of New Shares in <?= data.company.name ?> (the "Company")</b></para_1>
<? var totalCommitments = data.amount_raising || newIssues.TOTAL.money; ?>
<? var totalShares = newIssues.TOTAL._orig_shares ? newIssues.TOTAL.shares : null; ?>
<? // if we are issuing an equity instrument then we can count the number of shares. debt instrument get denominated in dollar amounts. ?>
<para_1>Notice is hereby given to all members of the Company that the Directors intend to issue up to <?= totalShares ? totalShares : totalCommitments+" of" ?> <?= data.security_type_plural ?> ("<?= data.security_type_short_plural ?>")<? if(data.price_per_share){?>, at a price of <?= data.price_per_share ?> per share<?}?><? if (totalShares) { ?>, for a total of <?= totalCommitments ?><? } ?>.</para_1>
<? if (data.explanation) { ?><para_2><?= data.explanation ?></para_2><? } ?>
<para_2>As a member of the Company, you possess the right under the Company's Constitution to participate in this issue.
<?if(data.preemptive_formality){?>We request that you waive this right, as your waiver is a necessary formality required for the Company to issue securities to <?=commaAnd(data.parties.new_investor,"name")?>.
<? xmLog("attempting to calculate percentage for sitout_shareholder %s", data.sitout_shareholder.name); ?>
<? var percentage = data.capTable.getActiveRound().sitout_shareholders[data.sitout_shareholder.name]._orig_shares / data.capTable.getActiveRound().pre_money._orig_shares; ?>
<? var pr_tf = 2;
  var pro_rata = (100*percentage).toFixed(pr_tf);
  while (pro_rata == 0) { pr_tf++; pro_rata = (100*percentage).toFixed(pr_tf); }
<? xmLog("preemptive_waiver: percentage=%s; pro_rata=%s",percentage, pro_rata); ?>
Given that you presently hold <?= data.capTable.getActiveRound().sitout_shareholders[data.sitout_shareholder.name].shares ?> out of <?= data.capTable.getActiveRound().pre_money.shares ?> existing shares, you have the right to subscribe for up to <?= pro_rata ?>% of this new issue: <?
if (data.security_essential == "equity") {
  var num_securities = percentage * newIssues.TOTAL._orig_shares;
<?= digitCommas_(num_securities, 0) ?> <?= plural(num_securities.toFixed(0), data.security_type_short_singular, data.security_type_short_plural) ?><?
  if (data.price_per_share) { ?> for <?= asCurrency_(currencyFor_(data.price_per_share), (percentage * newIssues.TOTAL._orig_money).toFixed(2)) ?><?}?>.
<? } else { ?>
<?= asCurrency_(currencyFor_(data.capTable.getActiveRound().amount_raised.money), (percentage * (data._orig_amount_raising || newIssues.TOTAL._orig_money)).toFixed(2)) ?> worth of <?= data.security_type_short_plural ?>.
<? } ?>
<? if (data._orig_amount_raising || newIssues.TOTAL._orig_money) { ?>You may also have excess rights to subscribe beyond that amount -- if other shareholders do not take up their pro rata allotments, you may participate beyond yours, up to the total amount of <?= totalCommitments ?>.
<? } ?>

For a given pair of (shareholder, round):

what securities do they currently hold?
and what are the details of those securities?
what is their history of holding their securities? (in other words, in which round did they acquire the securities, and how much did they pay?)
for each security held prior to the current round, what interaction arises with the current round
in terms of preemptive rights
in terms of EGM approval resolutions as a member
in terms of required notices
in terms of conversion
some classes of shares, like ESOP shares, may be dynamically calculated
at a given time, how many shares are restricted, and how many shares are unrestricted?

Best Practices

conversion immediately prior to a round is modeled as a separate round

Includes and Modules



InDesign Templates

Most of the documents generated by Legalese are contracts.


other weird templates

the structure of a typical document

character styles

paragraph styles


default values

output date

modifying the INDTs

you need InDesign. Sorry.

Internals Documentation

Be prepared: have you got your boots on? Because you’re about to wade into a Big Ball of Mud.

Walkthrough: what happens when:

the end-user opens the spreadsheet in their web browser?

The spreadsheet comes with a bound script: if you click on Tools / Script Editor a new tab will open showing the script. See code.js in the repo. On page load, this script automatically runs the onOpen function.

code.js’s onOpen() imports the LegaleseMain library. There are many versions of the library. If the end-user cloned the sheet recently, the most recent version will be loaded. If not, it is possible that the script is loading an old version of the library. To fix this, click Libraries and upgrade the library to the latest version.

If the end-user happens to be a Legalese developer, it is possible that the end-user has read/write access to the primary LegaleseMain library. If that’s the case, development mode will be enabled. If you see development mode, while your users don’t, you could get differing behaviour, and a source of apparent bugs.

The script also tries to load the LegaeseSignature library. See <a href=”*the end-user clicks @@html:@@Send to EchoSign@@html:@@?”>below.

After loading LegaleseMain, the script (code.js)’s onOpen calls the library (legaleseMain)’s onOpen. This function adds menu items to the Add-Ons menu.

the end-user edits cells?

Nothing. But Google Apps defines a high-resolution event handler which can be triggered with every update to a cell. We don’t declare this handler, but it is conceivable that in future we might want to create the illusion of a more interactive UI, for continuous passive input validation or other purposes.

the end-user clicks Generate PDFs?

Each menu item under Add-Ons / Legalese is bound to a function in the legaleseMain library. Generate PDFs is bound to fillTemplates.

fillTemplates lives in templates.js. First fillTemplates checks if it’s running in the controller context. If not, it reads the current sheet (using readRows()).

It reads from the CONFIG section which templates are desired.

To go from template name to template URL, it searches the Available Templates sheet of the Demo Master.

Once it has the template URL, it uses Google Apps’s HTML service to fetch the template contents.

It sets up a bunch of variables representing the data parsed from the Google Spreadsheet. It passes those variables to each template using Google Apps’s HTML Service.

It saves the resulting valid XML in the output folder in Google Drive. It shares the output folder with robot@legalese.io.

robot@legalese.io has an account on a Mac OS X server that sits somewhere off-cloud. That account has Google Drive installed: the My Drive folder is synced to the local hard drive.

Every minute, the robot account runs a Google Apps script to see if anybody has shared anything with it. If it sees any incoming folders, it “add to My Drive”. So when somebody shares a folder with the robot, that folder ends up on the local hard disk.

On the Mac, InDesign is running a script that monitors the Google Drive sync folder, looking for .xml files that don’t have corresponding .pdf files.

It processes those XML files into PDF. See https://github.com/legalese/legalese-indesign for more.

the end-user clicks Send to EchoSign?

If LegaleseSignature is not loaded, this menu option will not even appear. If it does appear, (for now) that probably means that the end-user is affiliated with Legalese in some way.

LegaleseSignature is not available to all users. The script contains EchoSign credentials for the Legalese Robot account at Echosign. Ideally in future we should set up a structure where the credentials are configured for PARTNER mode, so that the end-user uses their own EchoSign account, not ours, to send for signature.

legaleseSignature.js’s uploadAgreement function figures out which PDFs should have been generated by Generate PDFs. It confirms the PDFs exist, then shoves the set of generated PDFs into EchoSign using the EchoSign APIe.

How to Use inc_enumerate_parties

How to Customize inc_enumerate_signatures

Your top-level template should include inc_enumerate_signatures with the following invocation:

<?!= include("inc_enumerate_signatures", data, {rcpts_to:rcpts_to, _include2: {} }) ?>

By default, this is what happens:

  • for every To/Explode party role identified in the template’s Available Templates row, (e.g. “director”)
  • create a b_left_sidehead_nonum section header for the party role name, appropriately pluralized (e.g. “Directors”);
  • for each entity corresponding to the role (e.g. Alice, Bob, and Carol),
  • call inc_signature to create a signature box

There are a few things you can do to customize the behaviour of inc_enumerate_signatures:

Introductory Paragraph

Suppose you want to have a little paragraph at the top of the section for director. In your template spreadsheet, define

signature_lead_director: The following directors are signing on behalf of the Company.

Overriding the Section Header

Suppose the section header, by default, says one thing, and you want it to say another.

Maybe it says “New Investor”, and you’d rather it say “Series Seed Investor”.

Pass a term dictionary in the _include2 object:

<?!= include("inc_enumerate_signatures", data, {rcpts_to:rcpts_to, _include2: { term: { new_investor: "Series Seed Investor" } } }) ?>

Maybe the section header is “Director”, and you’d rather it say “On Behalf of the Company”.

<?!= include("inc_enumerate_signatures", data, {rcpts_to:rcpts_to, _include2: { term: { director: "On Behalf of the Company" } } }) ?>

Dealing with Pluralisations

The nouns in your section header are pluralised by default if there are more than one of each type of signatory: for example, if two directors are signing the document. If you would rather include a purely custom section header, pass override_section_header:true in the options object, and the renamed term for your signatory in the term object.

<?!= include("inc_enumerate_signatures", data, {rcpts_to:rcpts_to, override_section_header:true, _include2: { term: { director: "Company" } } }) ?>

No Section Header

To not display the b_left_sidehead_nonum section header at all, pass no_section_header:true in the options object.

<?!= include("inc_enumerate_signatures", data, {rcpts_to:rcpts_to, no_section_header:true, _include2: {} }) ?>

How does a spreadsheet resolve its signatories?

To an internals developer who’s glanced over templates.js, the signature generation logic may appear to be a black box riddled with callbacks and redundant data structures.

In this section, we open up the box and explain how the pieces fit together.

First, let’s look at what goes into the box. You should know by now that important template/party configuration lives in

  • the To field of an Available Templates row
  • the CC field of an Available Templates row
  • the Explode field of an Available Templates row

These three fields are of type PartyType: values might be “director”, “company”, “corporate_secretary”, and “new_investor”. These values correspond to the labels in Column A of the Entities and the Roles spreadsheet sections. They are role identifiers.

Let’s look at what comes out of the box.

At the top of a template, you may find <?!= include("inc_enumerate_parties", data, rcpts_to) ?>.

At the bottom of a template, you may find <?!= include("inc_enumerate_signatures", data, {rcpts_to:rcpts_to, _include2: {} }) ?>.

These include files are responsible for automatically expanding the roles into actual entities. If the Available Templates row specifies To: director,employee, where the company has three directors and two employees, then the template should have five signature boxes at the end.

Let’s work backward. How do those signature boxes come to appear?

They are produced by inc_signature.

inc_signature is called by inc_enumerate_signatures. It takes configuration from a variable called rcpts_to, which is set up by templates.js.

inc_enumerate_signatures is called by your top-level template.

Your top-level template is filled by the fillTemplate_ function, as part of an execution sequence ultimately triggered by the end-user clicking “Generate PDFs”.

fillTemplate_ is called by the buildTemplate function.

A few lines above the fillTemplate_ call, buildTemplate sets up the rcpts_to data structure as an attribute of the newTemplate itself. Notably, it also sets up two other attributes, .rcpts and .rcpts_cc. The important one for our purposes is .rcpts_to. That describes the party types (in this example, “director” and “employee”) and their expansions (the five actual entities).

rcpts_to comes out of rcpts. Where does rcpts come from?

rcpts is an argument to buildTemplate. Who passes the argument?

buildTemplate is run as a callback, called from within the docsetEmails_ object. In the normal case, docsetEmails_.normal() calls buildTemplate. In the exploder case, docsetEmails_.explode() calls buildTemplate.

Where do docsetEmails_.normal and docsetEmails_.explode come from? They are defined as methods of the docsetEmails object.

The docsetEmails constructor figures out who the To and CC email recipients should be, by reading the sourceTemplate.parties attribute. Some computation happens inside the constructor, to support special case syntax: To can be director[0], which means we engross only the first director listed in Entities, not all three directors.

After handling these special cases, the docsetEmails constructor parks to_list and cc_list in the this._rcpts attribute: those are just the email addresses. It also parks to_parties and cc_parties attributes into this._rcpts: the fullly expanded party entities.

When the docsetEmails.normal() and docsetEmails.explode() methods call the buildTemplate callback, they set up the rcpts argument to buildTemplate by calling docsetEmails.Rcpts() to access the appropriate values from the docsetEmails object.

So the call stack looks like this:

  • inc_signatures
  • inc_enumerate_[parties,signatures}
  • your top-level .xml template
  • fillTemplate_
  • fillTemplates.buildTemplate, running as a callback passed to
  • docsetEmails.normal() or .explode()
  • - (which use docsetEmails.Rcpts() along the way)
  • fillTemplates()

You recognize fillTemplates, of course. That’s the handler bound to the menu function “Generate PDFs”, in legaleseMain.js!

So now we’ve drawn a line from the top of the black box (clicking “Generate PDFs”) to the bottom of the box (getting XML and PDF output).

About Explosion

To recipients are pretty simple: expand all the parties and put them into rcpts_to.

Explode recipients are a little more complex: we generate a different PDF for each entity belonging to the exploding role.

About injection into e-signature

The above discussion describes template generation.

But that’s not the whole story. After a PDF is generated, it still needs to get injected into an e-signature backend.

And that injection needs to know who the To and CC parties are for each PDF.

(You can see the mapping between PDF to To/CC in the README Google Doc that’s produced in the Google Drive output folder.)

So, when the user clicks on the menu function “Send to Echosign”, the templates.js logic above needs to recompute the docsetEmails.rcpts(), except, this time, instead of handing off the resulting rcpts argument to a buildTemplate callback to generate PDFs, the rcpts needs to go to a callback which pushes the previously generated PDFs into the e-signature backend.

That would be the uploadTransientDocument callback. If you look inside legaleseSignature.js you will see a couple of lines inside uploadAgreement which call docsetEmails.normal() and docsetEmails.explode(). Those lines echo the insides of fillTemplate, except this time, uploadTransientDocument is the callback, not buildTemplate.

About the Company Spreadsheet

A Legalese Company Spreadsheet contains one or more sheets, appearing as tabs:

  • Entities
  • Cap Table
  • Incorporation
  • Seed Round
  • More Rounds

If you cloned the Legalese Demo Master, you will have something that looks like this.

Parsing Spreadsheets

Getting data from a spreadsheet into an internal Javascript representation is the job of a bunch of parsing modules.


readrows.js knows how to parse everything except the captable, which it delegates to captable.js


Let’s talk through what the captable parser does, step by step, to build all the other data structures available for access.

Here’s the call stack, from outside in:


var readRows_ = new readRows(sheet, entitiesByName,0);


this.capTable = new capTable_(sheet);

captable.capTable_() makes an initial pass over the capTable.

this.rounds = this.parseCaptable();

captable.parseCaptable() initializes the Rounds array

returns an array of Rounds.

Each Round is constructed by captableRounds.push(new Round(...))

Then, as investors are encountered who have either money or shares defined for the round, they are appended to the round’s new_investors and ordered_investors.

captable.Round(params) constructor

this constructor is pretty passive, it just copies the params key/value pairs into itself; the hard work of putting things into Round happens in parseCaptable.

params has keys name, new_investors, ordered_investors, sheet, captable.

captable.capTable_() populates round.old_investors

parseCaptable initializes the this.rounds object.

But we’re not done! After Rounds has been parsed and initialized, we still need to decorate. We make another pass over the rounds, populating additional attributes.

Because rounds build on one another, we track things in var totals, under subsidiary objects totals.all_investors and totals.by_security_type.

We also populate round.old_investors. Remember, an old_investor is someone who isn’t participating in the current round. So, how do we know what to say about them? We track each investor’s activity over multiple rounds in totals.all_investors. In a given round, when we see that they are not participating, we copy over a snapshot from the running total.

The running total tracks money and shares across multiple rounds. But, you say, “money might be in different currencies. And they might be subscribing for different types of securities. Squashing different types of things into a single bucket will get you in trouble!”

You are quite right. Now, if all the rounds for a given captable are denominated in the same currency, then we can have some confidence in the correctness of the money running total. And if a simple company has only ever transacted ordinary shares, then we believe the shares running total is correct.

But if a startup has different classes of shares – ordinary shares vs Class F vs Series Seed – then the number of shares maintained in the running total feels increasingly fragile. Sure, as long as all classes of shares are convertible 1:1 to ordinary, then, in theory, we’re OK. But in practice, it’s best to group the shares by security type, so we keep them separated.

That’s what the totals.by_security_type object does. It’s keyed by all the securities seen in the cap table. Then it’s keyed by investor name. Then the value is the number of shares, or money, or whatever. Wait, what? That’s right; we have a bit of type overloading in the value; if it’s denominable in shares, then the type is number of shares; if it’s not a share-type thing, like it’s a note or a SAFE, then the type is money.

That will get us in trouble one day, too.

Anwyay, after tracking these totals, we then copy a per-round snapshot to the round.by_security_type object.

captable.capTable_() sets up ESOP

Similar recordkeeping happens for ESOP.

If a special investor named “ESOP” is a new_investor in the round, then we set up an ESOP_ object.

round.ESOP = round.ESOP || new ESOP_(round.security_type, 0);

Note this quirk: if a participating investor appears below the ESOP line, we track them in the ESOP object, and their shares are considered part of the ESOP allocation. If they appear above the ESOP line we do not. This allows us to issue Class F vesting shares to founders without those shares being considered part of the ESOP allocation. In an early round, investors commonly consider there to be two kinds of vesting: founder vesting, and employee vesting. Legalese implements both these kinds of vesting using Class F, but only “employee” vesting is considered part of the 20% or 25% or whatever ESOP pool.

esop.ESOP_: constructor

captable.capTable_() populates round.brand_new_investors

This is a simple set operation: a brand_new_investor is a new_investor who was not previously an old_investor.


User Interface Sugar

We make it easy for parties to fill in their details.


The Controller

End-users usually run commands like Add-Ons / Legalese / Generate PDFs directly from the relevant tab of the spreadsheet.

But an administrator who manages several deals or companies at the same time may find it easier to use the Controller interface.

In the Controller, all tabs are listed on a single spreadsheet.

When a command is run, and the current sheet is a controller sheet, the command operates on whatever row is selected. More than one row may be selected.







Unused or under development






The Bound Legalese Script

In any Legalese Company Spreadsheet, if you go to Tools / Script Editor, you will see a bunch of code. This code references the LegaleseMain Library.

In the future we will release Legalese as a Google Add-On proper.

Then the script won’t be bound.



Every time you update a component, you will need to copy that component from your local editor into the Google App.

Library Version Management

If you make a significant change, you will need to, under Manage Versions, save a new version of the LegaleseMain library.

Then you MUST go to the Legalese Demo Master and in its Tools / Script Editor update the lirbray version.


At present this only works for Meng’s account to insert things into EchoSign. It contains his application keys and is therefore not really public.


the Makefile inserts Meng’s application keys into the actual built legaleseSignature.js.