Contract Automation
Combined reference — README · Apps Script User Guide · DevOps Notes
What this system does
The workflow is built around three legal documents and one master data schema. A Google Apps Script layer turns a set of staged Google Forms and a master Google Sheet into a working document pipeline.
The three documents
| Document | Parties | Purpose |
|---|---|---|
| Assignable Purchase Agreement (Ohio Residential) | Seller ⇄ Buyer (Assignor) | Core purchase contract; §13 expressly permits assignment. |
| Binding NDNCA | Assignor ⇄ Assignee | Non-disclosure / non-circumvention; liquidated damages = 2× total assignment fee. Must be signed before the Assignment Addendum is disclosed. |
| Assignment Addendum | Buyer (Assignor) ⇄ Assignee | Assigns the purchase contract; sets Initial and Settlement Assignment Fees. |
The master data schema
A single Google Sheet is the source of truth with three tabs:
| Tab | Name | Role |
|---|---|---|
| Tab 1.0 | Field Table |
Master data dictionary — one row per deal, ~90 mapped field columns grouped into Property, Financials, Dates & Inspections, Escrow, Seller, Buyer, Assignee, and Document Tracking sections. |
| Tab 2.0 | Form Responses |
Raw Google Form submissions land here. |
| Tab 3.0 | Milestone |
Process-flow / status reference. |
{{property_street_address}}, {{offer_price}}, {{assignee_name}}. That naming convention is what makes document autofill a straight find-and-replace.
The five-stage lifecycle
Four sequential Google Forms feed the pipeline. Each form is a superset of the prior stage; each stage produces a dynamic pre-fill URL for the next.
| Stage | Form | Captures | Produces | Drive folder |
|---|---|---|---|---|
| 1 — Property Offer | Form 1 | Property, offer price, seller | Master row, offer letter, Stage 2 pre-fill | 1_Autofill_Offer |
| 2 — Purchase Agreement | Form 2 | Earnest money, contingencies, parcel/legal, escrow, buyer | Purchase Agreement autofill, then the executed PA (held) | 2_Autofill_Purchase_Agreement, 3_Executed_Purchase_Agreement |
| 3 — NDNCA | Form 3 | Assignee entity, wholesale offer | NDNCA autofill. Executed NDA required before Stage 4. | 4_Autofill_Wholesale_Offer & NDA, 5_Executed_NDA_Assignment |
| 4 — Assignment & deposit | Form 4 | Assignment fees, end-buyer acceptance | Unexecuted addendum + diligence; then executed addendum and deposit invoice | 6_Assignor_Addendum, 7_Executed_Assignment_Invoice |
| 5 — Contingency & Settlement | — | (manual / downstream) | Settlement invoice, deed recording, archive | — |
5_Executed_NDA_Assignment. This keeps the seller's identity off the end buyer until the NDA is back.
Workflow folders
| Folder | Stage | What lands here |
|---|---|---|
1_Autofill_Offer | 1 | Offer letter |
2_Autofill_Purchase_Agreement | 2 | Draft purchase agreement |
3_Executed_Purchase_Agreement | 2 | Executed purchase agreement. Held until the NDA is executed |
4_Autofill_Wholesale_Offer & NDA | 3 | Wholesale offer and NDNCA draft |
5_Executed_NDA_Assignment | 3 | Executed NDA. Stage 3 is not complete without this file |
6_Assignor_Addendum | 4 | Unexecuted assignment addendum presented with the executed PA and diligence |
7_Executed_Assignment_Invoice | 4 | Executed addendum and the deposit invoice, after the deposit posts |
Paste each folder's Drive id on the workbook Deployment tab (FOLDER_1_AUTOFILL_OFFER through FOLDER_7_EXECUTED_ASSIGNMENT_INVOICE). The script copies drafts into folders 2, 4, and 6. Folders 3, 5, and 7 are the executed-file destinations.
Data flow
Google Form N ──submit──▶ Tab 2.0 (Form Responses)
│ ┌ button click / menu action ┐
▼ ▼
Tab 1.0 (Field Table, master row)
│ │
dynamic pre-fill URL │ generate agreements
for Stage N+1 form ▼
Purchase Agreement · NDNCA · Assignment Addendum
(Google Docs, saved to Drive folders)
Two things happen on the button click for each stage:
- The latest Form Responses row is copied into the matching row of Tab 1.0, with multi-select checkboxes converted to 1/0 binary encoding.
- A dynamic pre-fill URL for the next stage's form is generated and written back to the sheet.
Repository layout
apps-script/ Google Apps Script project (deliverable for this phase)
appsscript.json Manifest (scopes, timezone)
Config.gs Field map, stage rules, Deployment-tab loader
Menu.gs onOpen custom menu + button-assignable entry points
Ingest.gs Form Responses (Tab 2.0) → Field Table (Tab 1.0)
Prefill.gs Dynamic pre-fill URL generation (FormApp)
DocumentMerge.gs Field Table row → 3 agreement Google Docs (replaceText)
DriveManager.gs Copy generated docs into workflow Drive folders
Audit.gs Runtime discovery: tab layout, forms, config check
Utils.gs Shared helpers (checkbox→binary, row lookup, headers)
README.md Deployment & configuration guide
build-files/ Source workbook, Word templates, dev notes (not live ids)
templates/ Var1 purchase agreement, NDNCA, addendum (.docx)
workbook/ MASTER_WHOLESALE_AUTOMATION_WORKFLOW_var-1.1 (.xlsx)
notes/ Apps Script pre-fill notes and wholesale dev notes
docs/
FIELD_MAPPING.md Form Responses header → Field Table header reference
TEMPLATE_TOKENS.md {{token}} mapping for the three agreements
templates/ Tokenized markdown drafts used to build the Google Docs
README.md This file
Roadmap
| Phase | Scope | Status |
|---|---|---|
| 1 — Google Workspace Foundation | Forms → Sheet triggers, pre-fill URLs, document autofill, Drive filing | Complete |
| 2 — HubSpot CRM Integration | Deal pipeline, custom properties, forward/reverse sync | Planned |
| 3 — DocuSign & Payments | Template mapping, enforced signer order, invoices | In progress |
| 4 — Intelligence & Optimization (Real Estate Automation Framework) | Valuation agent, tax-card / deed scraping, dashboards | Complete |
Known limitations
- Google e-Sign cannot pre-populate signer fields and does not enforce signer order → resolved in Phase 3 via DocuSign.
- Google Forms cannot pre-fill from existing data → mitigated via dynamic pre-fill URLs; ideal resolution is CRM-driven entry in Phase 2.
- Property valuation (comps, ARV, tax card, deed/lien search) is manual → Phase 4.
Container-bound Google Apps Script for the master workbook MASTER_WHOLESALE_AUTOMATION_WORKFLOW_var-1.1. It moves data from the staged Google Forms into the Field Table and generates the three agreement documents.
Script files
| File | Responsibility |
|---|---|
appsscript.json | Manifest — OAuth scopes and timezone. |
Config.gs | Sheet names, field map, checkbox binary map, and the Deployment-tab loader. File and folder ids are not edited here. |
Menu.gs | onOpen custom menu + the functions you assign to in-sheet buttons. |
Ingest.gs | ingestStage1..4() — Form Responses (Tab 2.0) → Field Table (Tab 1.0). |
Prefill.gs | Dynamic pre-fill URL for the next stage's form (FormApp, by question title). |
DocumentMerge.gs | Field Table row → Purchase Agreement / NDNCA / Assignment Addendum. |
DriveManager.gs | Copy generated docs into workflow Drive folders; move-between-folders helper. |
Audit.gs | Runtime discovery: auditSheets(), auditForms(), checkConfig(), syncInspectionOptionsFromForm(). |
Utils.gs | Header maps, row lookup, checkbox→binary, toast/logging. |
What each button does
Ingest (per stage) — reads the latest row on the stage's response tab, maps each answer to its Tab 1.0 column, writes inspection indicators as true/false, and writes them to the deal's master row (Stage 1 creates the row; Stages 2–4 find it by property_street_address and update it). It then builds the pre-fill URL for the next stage.
Generate documents — reads the selected Tab 1.0 row and replaces every {{Column_Header}} token in each template with that row's value, filing the copy into the document's Drive folder and stamping the tracking date.
entry.<id> values. Prefill.gs matches each live question to a master field by question title via CONFIG.FIELD_MAP. You only supply each form's file id — the correct entry.<id> tokens are resolved at runtime and stay correct even if the form's ids change.
Deploy (container-bound)
- Open the master Google Sheet → Extensions → Apps Script.
- Create one script file per
.gshere and paste the contents (or useclasp, below). - Add
appsscript.json(enable Project Settings → Show "appsscript.json"). - Save, then reload the sheet — a Wholesale Automation menu appears.
- Run any item once and grant the OAuth scopes when prompted (Sheets, Docs, Drive, Forms).
Or with clasp
npm i -g @google/clasp
clasp login
clasp clone <SCRIPT_ID> # from the bound project's Project Settings
cp apps-script/*.gs apps-script/appsscript.json <clasp-dir>/
clasp push
Configuration checklist
Structural settings (tab names, FIELD_MAP, checkbox labels) stay in Config.gs. File and folder ids do not. They are pasted on the workbook Deployment tab. One workbook copy = one Drive deployment.
SHEETS.MASTER / RESPONSES / MILESTONEmatch your tab names → confirm withauditSheets().STAGES[n].responseSheet— one consolidatedForm Responsestab, or four separate tabs → confirm withauditSheets().Deployment!STAGE_1_FORM_ID…STAGE_4_FORM_ID— each form's/d/<id>/segment.Deployment!DOC_*_TEMPLATE_ID— the three tokenized Google Doc ids.Deployment!FOLDER_1_AUTOFILL_OFFERthroughFOLDER_7_EXECUTED_ASSIGNMENT_INVOICE— the workflow directory.CHECKBOX_FIELDS[0].options— confirm inspection labels withauditForms(), or auto-generate withsyncInspectionOptionsFromForm(2).- Confirm template tokens use
{{Header}}; otherwise changeCONFIG.TOKEN.
Run checkConfig() to list Deployment keys that are still blank.
Setup / audit workflow
From the sheet menu Wholesale Automation → Setup / audit, or from the Apps Script editor (pick the function → Run → View → Logs):
- auditSheets()
- Every tab, its headers, and whether responses are in one tab or four.
- auditForms()
- Each form's title and every question's title + type, including the exact checkbox choice labels (e.g. inspection options).
- syncInspectionOptionsFromForm(2)
- Prints a ready-to-paste
CHECKBOX_FIELDS[0].optionsblock built from the live form. - checkConfig()
- Lists Deployment-tab keys that are still blank.
- previewPrefillUrl(1, '<address>')
- Builds the Stage 2 pre-fill URL from a master row, to sanity-check title matching.
Wiring in-sheet buttons
Insert → Drawing (or Image) → draw the button → right-click → ⋮ → Assign script → type the function name (ingestStage1, …, generateAllAgreements). Everything is also in the Wholesale Automation menu.
Notes & assumptions
- Deals are matched across stages by
property_street_address(CONFIG.MATCH_KEY). For higher reliability, add aRecord_IDcolumn to Tab 1.0 and to each form (carried via the pre-fill URL), then setMATCH_KEYto it. - Ingest acts on the latest response row. For a true
onSubmittrigger, add an installable trigger callingingestStageN— but the button-click model matches the documented workflow and avoids double-processing. FIELD_MAPkeys must equal the live form question titles.auditForms()prints the live titles so you can confirm.
Follow-ups not yet applied to the GCP or Claude Co-Work runners. These notes record field-type and naming decisions that span the Apps Script layer and the broader pipeline.
Boolean indicators
general_inspection_ind, well_inspection_ind, pest_inspection_ind, radon_inspection_ind, and other_inspection_ind are HubSpot boolean checkboxes (true / false).
Apps Script writes true or false onto the Field Table. DocumentMerge.gs still prints X in the purchase agreement when the value is true. Legacy 1 / 0 values still count as checked so older rows keep working.
property_postal stays a string
property_postal is string / text on Contact and Deal, in the probate schema, and in RapidUpload. Leave that type until a numeric transform runs on every upsert. After that transform exists, change the schema field type. Do not change it before the transform, or ZIP+4 values will be rejected.
escrow_company_zip, seller_address_zip, buyer_zip, and assignee_address_zip.
property_last_transfer_date
The wholesale column is property_last_transfer_date, the same field the GCP and Claude runners already read and write for the auditor's last conveyance:
src/rapidupload/a2_runner.pysrc/rapidupload/b_runner.pysrc/rapidupload/auditor.py
The earlier worksheet name transfer_date was retired. That extra schema property was removed from the probate catalogs and from RapidUpload ingest properties. Wholesale now uses property_last_transfer_date.
parcel_number was not removed
Wholesale Parcel_Number is property_parcel on Contact and Deal. The live Deal property parcel_number is still present. The framework still uses it in config/pipeline.toml, existing_custom_fields.json, and the A1 listing extract. Removing it would change existing runner behavior, so it was left in place.
Two archive dates
document_archive_date and assignment_document_archive_date are both on the worksheet and in the portal schema. The assignment date can be tracked independently if the assignment addendum expires before transfer. document_archive_date was not renamed.
Record IDs
wholesale_record_id is the worksheet and portal id for this wholesale file. lead_uid is the existing portal UID. The HubSpot record id is unchanged. One record can exist on the portal and in another workflow, so these three ids stay separate.