You’re about to setup a private connection between your Google Docs, Sheets and a custom ChatGPT.

Paperclip Health
Kit + Setup Guide

🔒 No extra apps, no new logins.

Google Docs icon
Google Sheets icon
description Generate a summary of today’s notes for my doctor Pushed to Google Docs Draft follow-up questions for my doctor visit Ask ChatGPT grid_on Create a weekly meal plan from my nutrition sheet New tab in Google Sheets description Summarize insights from my exercise log Pushed to Google Docs grid_on Pull the latest lab results from Google Sheets New row in Google Sheets Log a new symptom: migraine today, started at 3pm. Ask ChatGPT

Watch the video walkthrough

timer Setup: 5 minutes

We use cookies and analytics to make this site work better.

Learn more

1.Copy Your Templates

chevron_right

1.1 Copy Google Doc + Sheet templates

To get started, make a copy of the Google Doc and Google Sheet templates. These work out of the box and include some example data to get you started.

1.2 View your Apps Script

Open the Apps Script editor from the Extensions menu
Apps Script workspace overview
Apps Script editor interface
imagesmode 1/3

In either your new Doc or Sheet, click Extensions > Apps Script from the top menu to view your Apps Script

Click the blue Deploy button and > New Deployment from the drop down menu.

info

“Paperclip 1.0.0” is the current Apps Script version. This is your unique copy that was generated alongside your new Google Doc + Sheet templates. It privately links your documents to your custom GPT.

2.1 Deploy Your Script

Apps Script workspace overview

In the New deployment window, click Deploy under the following configuration.

Select type: Web app
New description: Anything you like
Execute as: Me (your google account)
Who has access: Anyone (This allows your custom GPT to access. Don't worry, you'll secure it with a password in step 5.)

2.2 Authorize Your Script

Authorization prompt requesting Google permissions
Enter API key into configuration field
Enter API key into configuration field
imagesmode 1/3
info

You’ll run into a Google message saying “Google hasn’t verified this app”. This is normal for custom Apps Scripts. It does not mean the app is unsafe, only that Google hasn’t reviewed the individual script you’re about to deploy.

Why it’s safe:

You are the owner — The script runs entirely under your own Google account. No one other than you can access your documents and information.

Private by design — The script can only ask for permission to access the specific Docs and Sheets you specify. Nothing else.

Click Authorize and follow Google's verification steps.

To Publish: Click Advanced > Go to Paperclip 1.0.0 (unsafe) > Allow

2.3 Copy / Paste your published web app URL

Your google script is now published! Copy and paste the published web app URL into the box below so it's referenced in step 4.3.

3.1 Login and Create a Custom GPT

Visit OpenAI’s Explore GPT's page below, log in using your paid GPT Plus account or higher and click Create to open the custom GPT builder.

3.2 Configure

Click Configure and give your new custom GPT a Profile photo, Name and Description.

3.3 GPT Instructions Section

GPT instructions overview

Copy/paste the text below into the Instructions section.

View GPT instructions

Open and copy.

Instructions 
Paperclip | Google App Script | OpenAPI
v1.0.0 | Health

Startup handshake

First message:
Call modifyOrDiscover (POST to /exec):
If getConfig.securityMode = "secure" → prompt the user for their password.
If getConfig.securityMode = "convenience" → skip the password prompt and continue.
If getConfig.missing[] is non-empty → prompt the user only for those missing items. Never re-ask for items already configured.

First-Time Setup Flow

1. Welcome message (ConfigMissing or no password yet):
“👋 Welcome! Let’s get you set up. Please choose a password.”

2. Prompt order (friendly + dead simple):
Step 1 → Password (new, chosen by user)
Step 2 → Doc URL, Sheet URL and Timezone (optional, default UTC if skipped)

3. POST /exec config

{
  "action": "config",
  "docUrl": "…",
  "sheetUrl": "…",
  "password": "",
  "timeZone": "…"
}

4. Confirm
“✅ Setup complete — Password, Doc, Sheet, and Timezone linked.”
prompts (after setup):
- Give me the tour | Show my Dashboard | Log a new entry

Password Rules:
Plaintext password is never stored.
Script stores Doc ID, Sheet ID, and SHA-256 hash (if set at config).
Secure: always pass password in POST body.
Convenience: password may be omitted after hash is stored.
If reset → prompt: “Run /config to update your password.”

Normal Startup (already configured)

Call modifyOrDiscover (POST /exec):
Secure → { "action":"getConfig","password":"" }
Convenience → { "action":"getConfig" }
Then run searchFast:
Secure → { "action":"searchFast","password":"","requestId":"req-12345","params":{} }
Convenience → { "action":"searchFast","requestId":"req-12345","params":{} }
Render Dashboard with newest-first results.
Offer helpful prompts | actions
[Load full history] → run full search.
Skip getStatus.

Setup Triggers:
If you receive ConfigMissing, or Unauthorized with "Password not set", "Missing password", or "Invalid password":
- Prompt for any missing Doc/Sheet URL(s). If timeZone is missing, ask for it (optional).

After Setup Triggers resolve:
Call **modifyOrDiscover** (POST to `/exec`) with:
{ "action": "getConfig", "password": "" }

The getConfig response includes:
docId
sheetId
timeZone (nullable)
passwordAgeDays (integer, null if unset)
securityMode (secure | convenience)
warning (string, optional message if the password is old)

Reset actions (which = all/password/doc/sheet/tz) follow the same preview → confirm flow.
After reset, config will show missing[] and user will be prompted to reconnect missing parts.
If password is reset → GPT must prompt user: “run /config again to update your password.”
Config and Reset are gated by preview:true and must be confirmed via the same two-step flow.

Automatically load data (search modes)

- Fast search (default startup):
  {"action":"searchFast","password":"","requestId":"req-12345","params":{}}
  → Returns last 10 API-marked Doc entries (from the bottom of the Doc) 
    and last 10 rows from each Sheet tab (excluding WriteLog). 
  → Results are merged and sorted newest-first.
  → Use this for the initial Dashboard render.

- Full search (on demand):
  {"action":"route","routeAction":"search","password":"","params":{"source":"all","limit":300,"includeSections":true,"offset":0}}
  → Returns full history from Doc + Sheets. 
  → Supports offset/limit paging, sortByDate, and includeSections. 
  → Always loop with nextOffset until done:true.
  → If ResponseTooLarge/timeout, retry with limit:300.

Performance:
- Use searchFast at startup.
- Full search only on demand.
- Cache getConfig per session (first turn only).
- Support backoff on large/timeout. 
- Narrow scope: use params.tab for one sheet; source:"doc" or "sheet" when only one is relevant.


Confirm Rules (with Risk Scanner)

Confirm only if:
preview:true was issued first
requestId matches memory.lastPreview
Data normalized (2D arrays, headers checked)

Risk handling:
info / warning (including UndoReset) → show to user, allow confirmation if relevant
error → block unless user explicitly overrides
Note: risk scanner only applies to Sheet writes/sorts/deletes/updates.

AppendDoc Confirmation
On confirm, always resend the full structured payload, identical to preview (omit preview:true):
{
  "action": "route",
  "routeAction": "appendDoc",
  "password": "",
  "requestId": "req-5678",
  "text": "{\"date\":\"2025-09-04\",\"category\":\"Note\",\"summary\":\"Added a general update\",\"details\":\"Captured additional context\",\"tags\":[\"update\",\"general\"]}"
}

After confirm:
Clear memory.lastPreview
Log action
Return success

Write Behavior (Undo + Risks)

Preview: mini-table (Sheets) or structured snippet (Docs)
Risks: must be surfaced before confirm (Sheets only)

On every successful write (Doc or Sheet), always display the undo option in the success message.
Example:
✅ entry has been logged in your Feed.
Summary: Placeholder entry
[↩️ Undo]

Undo Flow: DONE → UNDO → UNDO_DONE
AppendDoc Undo = clears entry text, leaves paragraph structure intact.
UndoReset = warning: only actions after reset can be undone.
GPT must explicitly tell the user: “Only actions after this reset can be undone.”

Timezone Rules
Timezone: request → saved → default → UTC. Dates not auto-converted.

FSM:
Startup →
  getConfig →
    If ConfigMissing → First-Time Setup →
      Confirm success →
        Show onboarding prompts (once) →
        searchFast → Dashboard
    Else →
      searchFast → Dashboard

Full history expansion: route:search until done:true.
Writes: PREVIEW → CONFIRM → VERIFY → DONE.
Undo: DONE → UNDO → UNDO_DONE (blocked if UndoReset).

AppendDoc Undo = clears entry text, leaves paragraph structure intact

UndoReset = warning if WriteLog was recreated

Error Handling
ConfigMissing → run setup.  
Unauthorized → prompt for password/setup.  
TabNotFound/HeadingNotFound → show [Retry].  
TabExists → suggest new name.  
ServerError → show [Retry].  
PasswordAge warning → non-blocking warning.  
UndoReset → non-blocking warning.  

Categories (Health)
Symptom
Sleep 
Medication 
Diet 
Exercise 
Mental Health 
Reminder 
Appointment 
Screening/Test 
Lab 
Vital Signs 
Note

Disclaimer (Health)
→ Not Medical Advice.

Inference rules follow AppendDocPayload (date, category, summary are required; details, severity, nextSteps, and tags are optional).

Guardrails:
GPT must only suggest, preview, or confirm commands that exist in the API schema.
Never invent or recommend actions, functions, or parameters outside:
- `action` enums
- `routeAction` enums
If user request is unsupported and the script returns an error (UnknownAction, BadRequest, etc.):
- GPT should humanize the raw error message into contextually relevant, user-friendly language.
- Suggest the closest supported action/category if it’s obvious.
GPT must not imply that unsupported actions will work. All functionality is strictly limited to what exists in the API schema.
For Docs, the only supported write action is appendDoc (always appends to the bottom).
- GPT must never suggest deleting, moving, or inserting into existing Doc sections/headings.
- If requested, respond: “I can only add new entries to the bottom of your Doc — you’ll need to manually edit or restructure it yourself.”
- subtly encourage when the user writes new entries

Safety:
Never store user data
Operate only via user’s API
Only recommend commands from Command Reference
Always confirm before writing
Put the password only in the POST body, never in query or headers
info

These instructions teach your GPT how to authenticate, fetch information, and log updates reliably.

3.4 Add Some Conversation Starters

Add some conversation starters like open my dashboard, show all my data, and log an entry. You can change these in your custom GPT settings.

4.1 Create New Action

Scroll to the bottom of the configure page and click Create new action.

4.2 Authentication

Leave Authentication as None. You'll set a password in step 5.

4.3 Schema

Schema configuration panel

Copy/paste the text below into the Schema section.

Double-check that your Google Script web app URL has been included. (you pasted that earlier in Step 2.3)

View OpenAPI schema

Open and copy.

openapi: 3.1.0

servers:
  - url: https://script.google.com/macros/s/YOUR-URL-HERE/exec
    description: ⚠️ Replace the above with your own Google Apps Script deployment URL.

info:
  title: Paperclip | Google App Script | OpenAPI
  version: 1.0.0
  description: |
    API for interacting with user-linked Google Docs and Sheets (feed, logs, notes, structured data).

    ## Startup Flow
    - All calls are POSTed to `/exec` with a JSON body containing an `action` (and optional `routeAction`).
      There is no separate REST path for `getConfig` — use `POST /exec` with `{ "action": "getConfig", ... }`.
    1. POST `/exec` with `action=getConfig` → confirm Doc/Sheet IDs + timezone.
    2. POST `/exec` with `routeAction=search` → load data in pages (limit ~500) across Doc + Sheet, paging until `done:true`.
    3. Render Dashboard once at least the first page of results is loaded.
    
    ## Core Model
    - **Search is transport-only**: `routeAction: "search"` (or legacy `action: "search"`) streams raw doc lines and sheet rows.
    - The client should automatically page until `done:true` to cover 100% of docs + sheets.
    - The client does all filtering/analysis (dates, keywords, insights, linking).
    - All write actions require explicit preview/approval.
    - Undo supports restoring the last logged write for:
      - `appendRow` → deletes appended rows
      - `updateCell` → restores previous value
      - `updateRange` → restores old block
      - `deleteRow` → restores deleted row
      - `sortTab` → restores pre-sort row order
      - `createTab` → deletes created tab
      - `appendDoc` → **clears text of last API-appended note (with `` marker), leaving paragraph structure intact**
      - UndoReset → occurs if WriteLog was recreated. Undo is blocked for actions before that reset.

    ## Docs are Hybrid
    - Users may add headings manually.
    - The API appends timestamp-only feed entries at the bottom with an API marker.
    - Search works across both.

    ## Tab Versioning
    - Supply `params.baseTab` (to clone headers) and/or `params.suffix`
      (to auto-dedupe a " " name).
    - If `headers` are omitted and `baseTab` is present, headers are cloned.

    ## Setup
    - On first run, provide Doc URL + Sheet URL via `action=config`.
    - To initialize authorization, include `password` in this first `config` call so the backend can store a hash:
      `{ "action": "config", "docUrl": "...", "sheetUrl": "...", "password": "" }`
    - The client should then call `action=getConfig` to confirm values were stored.
    - After initial setup, subsequent `action=config` updates require the current `password` in the POST body.
    - The password is never accepted in query or headers—only in the POST body.
    - **Stored at rest:** Doc ID, Sheet ID, and a SHA-256 hash of the password

    ## Timezone
    - Supply `timeZone` (IANA) during `action=config` to persist a user preference (e.g., `America/Los_Angeles`).
    - `getConfig` returns the stored `timeZone`.
    - Any request may override with a per-request `timeZone`; otherwise uses stored → script default → UTC.

    ## Search Batching
    - The client should continue paging with `nextOffset` until `done:true`.
    - Start with `limit: 500` and reduce if responses are too large.

    ## FSM
    - Startup → getConfig → GotConfig
    - If ConfigMissing → Setup → SetupConfirmed → Onboarding → SearchFast → Dashboard
    - Else ConfigOK → SearchFast → Dashboard

paths:
  /exec:
    get:
      operationId: getStatus
      summary: Status ping.
      parameters:
        - name: statusCheck
          in: query
          required: false
          schema:
            type: string
            enum: ["true"]
      responses:
        '200':
          description: Status or OK payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetResponse'
    post:
      operationId: modifyOrDiscover
      summary: Run router, config, or search actions (Docs + Sheets).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostRequest'
            examples:
              getConfig:
                summary: Get configuration (Doc/Sheet IDs + timeZone)
                value:
                  action: getConfig
                  password: 
              configFirstRun:
                summary: First-time setup
                value:
                  action: config
                  password: mySecretPass123
                  docUrl: "https://docs.google.com/document/d/..."
                  sheetUrl: "https://docs.google.com/spreadsheets/d/..."
                  timeZone: "America/Los_Angeles"
              searchPage:
                summary: Search all data (first page)
                value:
                  action: route
                  routeAction: search
                  password: 
                  params:
                    source: all
                    includeSections: true
                    offset: 0
                    limit: 500
              searchFast:
                summary: Fast search (recent snapshot, newest-first)
                value:
                  action: searchFast
                  password: 
                  requestId: req-7890
                  params: {}      
      responses:
        '200':
          description: Result payload (config/search/route).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostResponse'

components:
  securitySchemes:
    PaperclipAuth:
      type: http
      scheme: bearer
      description: >
        OSWAP-style password hash authentication. 
        Password is only accepted in POST body (never query/header).
        Supports `secure` vs `convenience` modes.
      x-securityFeatures:
        - SHA-256 password hashing at rest (`_hash_`, `_secureEquals_`)
        - Brute force protection: lockout after 5 failures, 15m expiry (`_tooManyFailures_`, `_recordFailure_`)
        - Rate limiting: 20 writes per 60s per user (`_rateLimitWrites_`)
        - Preview → Confirm → Write enforcement (all write actions return `preview` first; `_wrapPreview_`, `_wrapOk_`)
        - Undo support for all write operations (`_undoLastWrite_`)
        - Replay / duplicate prevention via requestId cache (`_isDuplicateRequest_`, `_markRequest_`)
        - Risk scanner for formulas, merges, hidden rows/cols, filters, validations (`_runRiskScanner_`)
        - Execution guard: 5-minute max runtime, retries with backoff (`_withExecutionTimeLimit_`, `_withRetry_`)
        - Locking for atomic operations (`_withLock_`)
        - Audit log of all writes (WriteLog tab, auto-pruned at 200 entries) (`_logWriteAction_`, `maybePruneWriteLog`)
        - Append-only doc writes with API markers + safe undo (`_appendToDoc_`, undo appendDoc branch)
        - Config protection and reset helpers (`_setConfig_`, `_resetConfig_`)
        - Timezone enforcement with IANA compliance (`_resolveUserTimezone_`, `_getIsoTimestamp_`)
  schemas:
    AppendDocPayload:
      type: object
      required: [date, category, summary]
      properties:
        date:
          type: string
          format: date
          example: "2025-09-04"
        category:
          type: string
          example: "Note"
        summary:
          type: string
          example: "Added a general update"
        details:
          type: string
          example: "Captured additional context and background information"
        severity:
          type: string
          example: "Medium"
        nextSteps:
          type: string
          example: "Follow up on this item tomorrow"
        tags:
          type: array
          items:
            type: string
          example: ["update", "general", "log"]

    DashboardResponse:
      type: object
      description: "Standardized dashboard output with Insights, Reminders, Updates, and Quick Actions."
      required: [insights, reminders, updates, quickActions]
      properties:
        insights:
          type: array
          description: "💡 Insights — summarized most relevant findings"
          items:
            type: string
        reminders:
          type: array
          description: "📅 Reminders & Upcoming Events — tests, screenings, checkups"
          items:
            type: string
        updates:
          type: array
          description: "📝 Recent Updates — last ~3 months"
          items:
            type: string
        quickActions:
          type: object
          description: "Quick Actions panel"
          required: [search, edit, settings]
          properties:
            search:
              type: array
              description: "Search options"
              items:
                type: string
                enum: ["Full Search", "Keyword", "Feed", "Facts and Insights"]
            edit:
              type: array
              description: "Edit options"
              items:
                type: string
                enum: ["Add Note", "Create Tab", "Append Rows", "Update Cell"]
            settings:
              type: array
              description: "Settings options"
              items:
                type: string
                enum: ["Change Settings"]

    GetResponse:
      type: object
      required:
        - status
        - version
        - action
        - ts
      properties:
        status:
          type: string
          enum: [success, error]
          description: |
            - success → operation executed
            - error → validation, config, or server failure
        version:
          type: string
          description: API version of the response
        action:
          type: string
          description: Echoed back action value (e.g. search, config)
        ts:
          type: string
          format: date-time
          description: ISO timestamp of the response
        message:
          type: string
          description: Human-readable message
        type:
          type: string
          enum:
            - Unauthorized
            - SourceNotFound
            - BadRequest
            - UnknownAction
            - HeadingNotFound
            - TabNotFound
            - TabExists
            - ServerError
            - ConfigMissing
            - UndoReset
          description: Error or warning classification
        doc:
          type: string
          description: Google Doc ID currently linked
        sheet:
          type: array
          description: Google Sheet tabs currently linked
          items:
            type: object
            additionalProperties: true
        suggestions:
          type: array
          description: Optional suggestions for next step recovery
          items: { type: string }
        docId:
          type: string
          description: Persisted Doc ID
        sheetId:
          type: string
          description: Persisted Sheet ID
        timeZone:
          type: [string, "null"]
          description: Persisted user timezone (IANA) if set via `config`.
        passwordAgeDays:
          type: integer
          description: Age of the current password in days (null if unset)
          example: 187
        warning:
          type: string
          description: Friendly warning message if the password is old
          example: "⚠️ Your password is 187 days old."
        missing:
          type: array
          description: Components not currently configured
          items:
            type: string
            enum: [doc, sheet, tz, password]

    PostRequest:
      type: object
      description: >
        Payload for all `/exec` POST calls.  
        - All actions **except** `config` require `password` in the POST body.  
        - The password is never accepted in query or headers.  
        - For `action=config`: 
          • On first run: `docUrl` and `sheetUrl` are required; `password` is optional.  
          • On subsequent runs: `docUrl`, `sheetUrl`, and `password` are all required.  
      required:
        - action
      properties:
        password:
          type: string
          description: |
            Must be included in the POST body (never query/header).
            - For `config`: required to update values; optional on first run.
            - For `getConfig`: optional (only needed if no password stored yet).
            - For all other actions: required.
        action:
          type: string
          enum: [listTabs, listDocHeadings, route, search, searchFast, config, getConfig, reset]
          description: |
            - `search`: General full history search.  
            - `searchFast`: Fast snapshot of recent entries (last ~10 from Doc + each Sheet tab).  
            - `getConfig`: Returns stored Doc/Sheet IDs, timezone, passwordAgeDays, and warnings.  
            - `config`: First-time or updated configuration.  
            - `reset`: Clears stored config values (doc, sheet, tz, or password).
        securityMode:
          type: string
          enum: [secure, convenience]
          description: |
            Controls password entry mode:
            - secure → GPT must prompt for password each session.
            - convenience → password stored until reset.
        which:
          type: string
          enum: [all, password, doc, sheet, tz]
          description: Required when `action=reset`.
        routeAction:
          type: string
          enum:
            - appendDoc
            - getSheet
            - appendRow
            - updateCell
            - updateRange
            - deleteRow
            - deleteRows
            - createTab
            - getHeaders
            - undo
            - sortTab
            - search
        requestId:
          type: string
          example: req-12345
          description: Unique request ID for deduplication and preview/confirm pairing.
        timeZone:
          type: string
          example: America/Los_Angeles
          description: |
            IANA timezone string.  
            With `action=config`: persisted as user default.  
            With other actions: overrides effective timezone for that request only.
        preview:
          type: boolean
          example: true
          description: |
            If true, sensitive actions (`config`, `reset`, all writes) return a **non-destructive preview**:
            - `preview` for config/reset and all write actions.
            Re-run without `preview:true` to confirm.
        tabName:
          type: string
          example: reminders
        headers:
          oneOf:
            - type: string
            - type: array
              items: { type: string }
        rowData:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
            - type: array
              items:
                oneOf:
                  - type: string
                  - type: number
                  - type: array
                    items:
                      oneOf:
                        - type: string
                        - type: number
        row:
          type: integer
          example: 5
        col:
          type: integer
          example: 2
        value:
          oneOf:
            - type: string
            - type: number
            - type: array
              items:
                oneOf:
                  - type: string
                  - type: number
                  - type: array
                    items:
                      oneOf:
                        - type: string
                        - type: number
        section:
          type: string
          example: General
        text:
          type: string
          description: |
            For `routeAction=appendDoc`, this must be a **JSON-encoded string**  
            matching the `AppendDocPayload` schema.
          example: |
            "{\"date\":\"2025-09-04\",\"category\":\"Note\",\"summary\":\"Added update\"}"
        matchMode:
          type: string
          enum: [loose, exact]
          default: loose
        headingLevel:
          type: integer
          example: 2
        params:
          type: object
          description: Extra parameters for search and tab operations.
          properties:
            source:
              type: string
              enum: [doc, sheet, all]
              default: all
            tab:
              type: string
            offset:
              type: integer
              default: 0
            limit:
              type: integer
              default: 500
              maximum: 10000
            includeSections:
              type: boolean
              default: true
            filterDate:
              type: string
              format: date
              example: "2025-07-15"
            sortByDate:
              type: boolean
              default: false
            baseTab:
              type: string
              description: Clone headers from this tab if headers omitted
            suffix:
              type: string
              description: Auto-dedupe name by appending suffix (e.g., " v2")
            ascending:
              type: boolean
              example: true
            rows:
              type: array
              items: { type: integer }
              example: [5,7,12,13,20]
            startRow:
              type: integer
              example: 10
            count:
              type: integer
              example: 25
        docUrl:
          type: string
          description: Required when `action=config`.
        sheetUrl:
          type: string
          description: Required when `action=config`.


    PostResponse:
      type: object
      required:
        - status
        - version
        - action
        - ts
      properties:
        status:
          type: string
          enum: [success, error, preview, warning]
          description: |
            - success → confirmed operation
            - error → validation or server failure
            - preview → dry-run of write action
            - warning → confirmation prompt
        version: { type: string }
        action: { type: string }
        ts: { type: string, format: date-time }
        message: { type: string }
        type:
          type: string
          enum:
            - Unauthorized
            - BadRequest
            - UnknownAction
            - HeadingNotFound
            - TabNotFound
            - TabExists
            - SourceNotFound
            - ServerError
            - ConfigMissing
            - UndoReset
        suggestions:
          type: array
          items: { type: string }
        tabs:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              gid: { type: integer }
              rows: { type: integer }
              cols: { type: integer }
              headers: { type: array, items: { type: string } }
        headings:
          type: array
          items:
            type: object
            properties:
              index: { type: integer }
              level: { type: integer }
              title: { type: string }
        source: { type: string, enum: [doc, sheet, all] }
        items:
          type: array
          items:
            oneOf:
              - type: object
                properties:
                  type:
                    type: string
                    enum: [doc]
                  idx:
                    type: integer
                  text:
                    type: string
                  section:
                    type: [string, "null"]
                  headingLevel:
                    type: [integer, "null"]
                  hintDate:
                    type: [string, "null"]
                    format: date
              - type: object
                properties:
                  type: { type: string, enum: [sheet] }
                  tab: { type: string }
                  rowIndex: { type: integer }
                  data:
                    type: object
                    additionalProperties: true
                  a1Range: { type: string }
                  hintDate:
                    type: [string, "null"]
                    format: date
        nextOffset:
          type: [integer, "null"]
        done: { type: boolean }
        verification:
          type: object
          properties:
            verified: { type: boolean }
            actualHeaders: { type: array, items: { type: string } }
        undoPreview:
          type: object
          properties:
            action: { type: string }
            tab: { type: string }
            row: { type: string }
            col: { type: string }
            value: { type: string }
            oldValue: { type: string }
            oldRowJson: { type: string }
            cleared:
              type: array
              items: { type: string }
              description: Paragraph texts cleared when undoing appendDoc. Paragraph nodes remain.
          description: |
            Preview of last action that would be undone.
            If `type=UndoReset`, no undo is available — response will only contain
            an error with `status: error` and `type: UndoReset`.
        tab:
          type: [string, "null"]
        baseTab:
          type: [string, "null"]
        suffix:
          type: [string, "null"]
        headers: 
          type: [array, "null"]
          items: 
            type: string
        risks:
          type: array
          items:
            type: object
            properties:
              type: { type: string }
              severity: { type: string, enum: [info, warning, error] }
              message: { type: string }
        rows:
          type: array
          description: Row indices targeted (deleteRows preview)
          items: { type: integer }
        count:
          type: integer
          description: Number of rows targeted (deleteRows preview)
        perRow:
          type: array
          description: Row snapshots for preview (deleteRows)
          items:
            type: object
            properties:
              row: { type: integer }
              rowData:
                type: array
                items:
                  oneOf:
                    - type: string
                    - type: number
                    - type: boolean
                    - type: "null"
        risksByRow:
          type: array
          description: Risk list per row (deleteRows preview)
          items:
            type: object
            properties:
              row: { type: integer }
              risks:
                type: array
                items:
                  type: object
                  properties:
                    type: { type: string }
                    severity: { type: string, enum: [info, warning, error] }
                    message: { type: string }
        deletedRows:
          type: array
          description: Rows deleted (deleteRows success)
          items: { type: integer }
        saved:
          type: object
          properties:
            docId:
              type: string
              description: Persisted Doc ID set in config
            sheetId:
              type: string
              description: Persisted Sheet ID set in config
            timeZone:
              type: [string, "null"]
              description: Persisted timezone if set
            securityMode:
              type: string
              enum: [secure, convenience]
              description: Persisted security mode if set
        docId: { type: string }
        sheetId: { type: string }
        passwordAgeDays:
          type: integer
          description: Age of the current password in days (null if unset)
          example: 187
        warning:
          type: string
          description: Friendly warning message if the password is old
          example: "⚠️ Your password is 187 days old."
        timestampUsed:
          type: string
          format: date-time
          description: |
            ISO8601 timestamp with timezone resolved via per-request `timeZone` or stored user timezone.
            Included in preview and write responses. Not present for read-only actions..
        pruneMessage:
          type: string
          description: |
            Optional. Present if WriteLog entries were auto-pruned during search.
          example: "Auto-pruned 87 old entries from WriteLog on startup."
        missing:
          type: array
          description: Components not currently configured
          items:
            type: string
            enum: [doc, sheet, tz, password]

    FSM:
      type: object
      description: |
        ## FSM Guidance (for GPT reasoning)
        This finite state machine (FSM) describes the expected startup flow and ensures onboarding 
        prompts are shown only once, after which the user should go directly to the Dashboard.

        ### State Transitions
        - **Startup**
          - Call `getConfig` → transition to **GotConfig**

        - **GotConfig**
          - If result = `ConfigMissing` → transition to **Setup**
          - If result = `ConfigOK` → transition to **SearchFast**

        - **Setup**
          - Once `SetupConfirmed` → transition to **Onboarding**

        - **Onboarding**
          - Once `OnboardingShown` → transition to **SearchFast**

        - **SearchFast**
          - Once `DashboardRendered` → transition to **Dashboard**

        - **Dashboard**
          - Final state — dashboard loaded and user ready for interaction

        ### Notes for GPT
        - Always begin with `getConfig`.
        - Handle `ConfigMissing` before proceeding.
        - Only show onboarding once (after Setup).
        - After Dashboard is loaded, skip back to Dashboard directly on future runs.
      properties: {}
      additionalProperties: true
info

The OpenAPI schema is a standardized file format which allows your custom GPT to communicate with your Apps Script reliably. The Schema helps define the API's' structure and capabilities.

4.4 Turn Coversation data off

Go back to the configure page. At the very bottom under Additional Settings, untick "Use conversation data in your GPT to improve our models".

4.5 Create and Save

Click Create (top-right). Set sharing to Only me, then click Save.

5.1 Set your password, docs and timezone

Confirm linked documents and timezone

On first startup, your custom GPT will ask you to set a password, then confirm your Google Doc and Sheet as well as your timezone.

You're Done! 🎉

Your Custom GPT is now fully set up and connected!

Frequently Asked Questions

⚙️ Setup & Config chevron_right
Why do I see a ConfigMissing error? chevron_right

This just means something hasn’t been connected yet. Your Doc, Sheet, timezone, or password. Run setup again with your Doc + Sheet links and password to finish connecting.

Google says “This app isn’t verified” — is it safe? chevron_right

Yes. This script runs entirely under your Google account. The warning shows up because Google hasn’t reviewed it, not because it’s unsafe.

My Doc or Sheet isn’t showing up. What now? chevron_right

Open your config status — if Doc or Sheet are missing, they’ll be listed. Just run /config again with the correct link. If you’ve recently used /reset, you’ll need to reconnect it too.

Why do I need a password? chevron_right

Your password protects your tracker. You choose it during setup — only a one-way hash is stored, never the actual password. If you see “password missing,” just run /config to set or reset one.

What’s the difference between secure and convenience mode? chevron_right

Secure mode asks for your password every time. Convenience mode skips the prompt once a password is stored. You can switch between them any time with /config.

How do I reset everything? chevron_right

Use the /reset command. You can reset everything (all) or just one piece: password, doc, sheet, or tz. After resetting, reconnect using /config.

Why am I seeing a password age warning? chevron_right

You’ll get a friendly reminder once your password is over 90 days old. It’s optional, but if you’d like, you can reset it any time with /reset password.

Why is my timezone wrong in logs? chevron_right

If no timezone is set, the tracker defaults to UTC. Add or update your timezone in /config (use a standard name like America/Los_Angeles).

🔑 Password & Authorization chevron_right
Why do I see “Unauthorized” or “Missing password”? chevron_right

This means your password hasn’t been set, or wasn’t included with the request. Run /config to create one, then include it when prompted (unless you’re in convenience mode).

My password isn’t working. What should I do? chevron_right

Double-check you’re entering the same one you set during setup. If you’ve forgotten it, run /reset password and then set a new one with /config.

What’s “convenience mode” and why didn’t it ask me for a password? chevron_right

In secure mode you’ll always be asked for your password. In convenience mode, once a password is stored, you won’t be asked every time. You can switch between modes in /config.

I typed the wrong password too many times — now I’m locked out chevron_right

For security, the script blocks login after several failed attempts. Wait 15 minutes and try again, or reset your password if needed.

How do I reset my password? chevron_right

Run /reset password. Then use /config to set a new one. Your Doc and Sheet connections will stay linked.

📄 Docs & Sheets Issues chevron_right
Why do I see “TabNotFound” or “HeadingNotFound”? chevron_right

That means the tab or heading you asked for doesn’t exist yet. In Sheets, check the tab name is spelled exactly right. In Docs, only existing headings are recognised — new ones can’t be created automatically.

Why do I see “TabExists”? chevron_right

A sheet tab with that name already exists. Try adding a suffix like v2 or –Updated. The system won’t overwrite an existing tab.

Why can’t I delete or reorder entries? chevron_right

For Docs: you can only add new entries to the bottom — deleting or moving existing text must be done manually in Google Docs. For Sheets: you can update cells or delete rows using the built-in actions, but full reordering is still best done directly in Google Sheets.

🔄 Performance & Reliability chevron_right
Why is my dashboard slow to load? chevron_right

At startup the app runs a quick search (last 10 Doc entries + last 10 rows per tab). Loading full history takes longer because results are fetched in chunks (usually 300–500 rows at a time).

What happens if the response is too large or times out? chevron_right

The script will retry automatically if Google’s backend is busy. If the request is genuinely too large, it suggests reducing the batch size or splitting the request into smaller chunks.

What does “ServerError” mean? chevron_right

It usually means Google Docs/Sheets had a temporary hiccup. Most of the time, retrying the action a few moments later works fine.

Are there any limits on how much I can write? chevron_right

Yes — to protect your Sheet, the script enforces a soft limit of about 20 write operations per minute. If you hit this, wait a moment before trying again, or batch your writes together.

Will my WriteLog keep growing forever? chevron_right

No — the app automatically trims older WriteLog entries if the log grows too large. You’ll always keep the most recent history for undo, but it won’t bloat your Sheet.

🔒 Privacy & Security chevron_right
Where is my data stored? chevron_right

Everything stays in your own Google account (Docs + Sheets). Nothing is sent to external servers — the script only connects your Google Workspace with your GPT account.

Is my password safe? chevron_right

Yes. Your Paperclip password is never stored in plaintext. The script only keeps a one-way SHA-256 hash inside Google’s secure PropertiesService. You can reset it anytime with /reset password.

Why do I see “Google hasn’t verified this app”? chevron_right

This is normal for private Apps Script projects. It means Google hasn’t formally reviewed your script — not that it’s unsafe. Because you own and deploy it yourself, only you can run it.

Can anyone else access my Docs or Sheets? chevron_right

No. Only you (the Google account that deployed the script) can access them. The script never shares your content with anyone else.

What if I want to revoke access? chevron_right

You can revoke permissions anytime in your Google Account → Security → Third-party access. You can also delete the script deployment, which fully removes all stored config (Doc, Sheet, password hash, timezone).

What security measures are built in? chevron_right

The script follows best-practice security standards, including: • Password hashing (SHA-256) • Brute force lockout after repeated failures • Rate limiting on writes (20/minute) • Input sanitisation to block unsafe data • A built-in Risk Scanner for sheet edits • Preview → Confirm → Undo flow for all writes • Automatic pruning of the WriteLog to keep history lean

How is this different from other GPT connectors? chevron_right

Most SaaS connectors store and process your data on their servers. Paperclip never does. Everything runs privately in your Google Workspace and your own GPT account — 100% transparent and open-source.

✅ Undo & Recovery chevron_right
Can I undo an action? chevron_right

Yes — every write action is logged, and you’ll see an Undo option right after it runs. • For Sheets: undo restores the last row, cell, or block that was changed. • For Docs: undo clears the last entry added by the API, but won’t change existing text or structure. Undo always applies to the most recent action only.

What does “UndoReset” mean? chevron_right

A reset clears the undo history. UndoReset means only actions made after your last reset can be undone — anything before that is permanent.

Does the undo history grow forever? chevron_right

No — the app automatically trims older WriteLog entries once the log gets too big. You’ll always keep the most recent history, but the Sheet won’t get bloated.

Have Feedback or Need Support?

We’re here to help! If you have any questions, don’t hesitate to reach out.

If you’re finding the tool useful, we’d love it if you left us a review.

Need an extra hand? We offer setup support for a small fee — perfect if you’d like help getting started or need something a little more custom.