Skip to content

File uploads ​

How gform fills Google Forms file upload questions — YAML sources, auto-apply vs assist, Drive chips, mime/size gates, artifacts, and decision logs.

Related: schema · commands/run · artifacts · limitations · agents

Docs: thegform.pages.dev/file-uploads

Mental model ​

A file step does one of two things:

  1. Configured source → gform resolves bytes, checks mime/size, tries Playwright setInputFiles, copies into artifacts/<id>/content/, status uploaded.
  2. No source (assist-only) → headed + TTY pause; you upload in the browser; status assisted (or skipped if nothing attached). Headless without a source fails with assist-headed-required.

gform does not drive the Google Drive picker UI. You either point YAML at bytes (local / gdrive / remote) or upload yourself under assist.

Prefer source: (three types) ​

yaml
- action: file
  question: Resume
  source:
    type: local          # or gdrive | remote
    path: ./files/resume.pdf
  accept: [pdf]
  maxBytes: 10485760     # optional size gate (bytes)
source.typeRequired fieldsWhat run does
localpath:Read from disk (cwd-relative or absolute)
gdriveid: and/or link:Parse Drive file id → download with the auth session (--auth) → cache → attach
remoteurl: (https)Fetch once into $GFORM_HOME/cache/files → attach

Use whichever you have. If both are set, id wins. If only link is set, gform parses the file id.

yaml
# Explicit id (from Forms chip data-id, or share URL path)
source:
  type: gdrive
  id: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms

# Or a share / open link — id is parsed
source:
  type: gdrive
  link: https://drive.google.com/file/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/view?usp=sharing

# Both ok — id used; link kept for humans
source:
  type: gdrive
  id: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms
  link: https://drive.google.com/file/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/view

Parsed link shapes (non-exhaustive):

Link shapeParsed?
drive.google.com/file/d/<ID>/…yes
drive.google.com/open?id=<ID>yes
drive.google.com/uc?id=<ID>&export=downloadyes
aistudio.google.com/…?state={"ids":["<ID>"],…}&usp=drive_linkyes — Drive Share → Copy link often opens AI Studio with the id in state
Bare id (A-Za-z0-9_-, length ≥ 10)yes (as id:)
`docs.google.com/documentspreadsheets

Prefer a bare id: when you can (most reliable). gdrive fetch needs a live browser + Google cookies → run with --auth (or an active profile). Offline gform verify does not download Drive bytes (tip points at gform run … --auth). Sync live Forms accept categories with gform verify <form> --check (add -A when the form needs auth).

Assist-only ​

yaml
- action: file
  question: Document
  # no source
  accept: [pdf]   # optional; still used if you later add a source

Requires --headed (and a TTY). Headless → hard fail assist-headed-required (not a fake success).

Decision map (what run logs) ​

Every auto choice is a content · decide · … notice (operators + agents). Flow:

text
file step
  ├─ has source / pool|urls?
  │    ├─ no  → decide: assist-only → headed wait OR headless fail
  │    └─ yes → decide: apply source.type=local|gdrive|remote
  │         ├─ resolve bytes (disk / cache / Drive fetch)
  │         ├─ decide: mime ok|fail (accept YAML ∪ live Forms)
  │         ├─ decide: size ok|fail (if maxBytes set)
  │         ├─ try attach (Add File → Drive picker iframe setInputFiles / filechooser)
  │         │    ├─ Selected-files chip → decide: uploaded · status=uploaded · copy content/
  │         │    └─ no chip → decide: assist-fallback (headed) OR fail
  │         └─ resolve error → decide: assist-fallback (headed) OR fail
  └─ after assist Enter
       ├─ Selected-files chip present → status=assisted (+ best-effort Drive fetch)
       └─ empty → status=skipped (not success for --once / unique)
Decide tagMeaning
applyAbout to resolve this source.type
mime-ok / mime-failAccept gate
size-ok / size-failmaxBytes gate
uploadedsetInputFiles succeeded
assist-onlyNo configured source
assist-fallbackAuto path failed; headed assist next

With -pa / --prefer-ask (headed + TTY), mime / size / fetch fails become a rematch menu: another local path, Drive id|link, remote url, raise maxBytes, assist, skip, or abort — instead of silent apply-or-fail. Headless → warn and ignore -pa.

Accept + maxBytes ​

FieldRole
accept: [pdf, image, .png, pptx, …]Overrides live Forms categories when set; bare exts (pptx) and dotted (.pptx) both work; mime-checked on verify / edit save / run
maxBytes: NReject after resolve if file is larger (remote + gdrive + local)

gform verify and edit --ask Save use the same gates (gdrive download deferred until run). After the form owner changes allowed types, run gform verify <form> --check -A to widen YAML accept and fill missing maxBytes / maxFiles from hydrated live hints (Upload N … Max N MB is per file; form-wide Drive quota is separate).

Pools / variants ​

FieldEffect
pool: [./a.pdf, ./b.pdf]Per-run local path → resolved as source.type=local
urls: [https://…, …]Per-run remote URL → source.type=remote
Templates in source.path / source.url / source.id / source.linkSame &#123;&#123;i&#125;&#125; / &#123;&#123;run&#125;&#125; rules as other steps

options.variantMode: rotate · random · zip — see repeat.

Drive chip detect (assist / page already has a file) ​

When the Forms Selected files list has a listitem (jsname="XPtOyb", data-id, filename):

  • Empty list + visible Add file → nothing attached
  • Listitem present → gform records the chip; may fetch Drive bytes with the auth session into artifacts/<id>/content/
  • That is detect + download, not auto-picking from My Drive

Do not confuse with YAML source.type: gdrive, which you configure up front.

Artifacts ​

PathWhen
artifacts/<id>/content/Bytes copied after successful resolve + attach (or Drive chip fetch)
manifest.content[]Per-question: source, mime, sha256, bytes, status

Statuses: uploaded · assisted · skipped. Failed assists are not success — --once / -sd / unique stay clean.

gform run view <id> prints a Content section when present.

Headed vs headless cheat sheet ​

SituationHeaded + TTYHeadless
source applies + settable inputauto uploadauto upload
source applies but Drive-only UIassist pausefail assist-headed-required
no sourceassist pausefail assist-headed-required
soft wall (CAPTCHA, …)assistfail (never pauses)

Use --headed -ko while teaching uploads. --auth for login-walled forms and gdrive downloads.

Headless uploads can still fail when Google does not expose a settable input — that is an external UI limit, not a silent success. Prefer headed assist while diagnosing.

Accept categories ​

Tokens may be categories, MIME types, or extensions (pptx / .pptx):

Category aliases (examples)Covers
pdfapplication/pdf
image / video / audiomatching MIME families
documentWord / ODT / text / rtf / pdf, …
spreadsheetExcel / ODS / csv, …
presentationPowerPoint / ODP, …
drawingimage/ + pdf

YAML accept overrides live Forms categories when set; run/verify still mime-check resolved bytes. Live hints sync via gform verify <form> --check (add --auth when needed).

Concurrency and picker risk ​

Authenticated file uploads under high --concurrency are a known stress point:

  • Multiple workers can race on picker iframes / newly created inputs
  • Google Drive cache hits are helpful but must stay concurrency-safe (hardening on the roadmap)
  • Recommended while debugging: concurrency 1, --headed if assist may be required, --pw-trace on failures
  • Do not load-test third-party forms — use forms you own

gform does not claim flawless file uploads. Failed attaches should leave decide logs, manifests, and screenshots when possible.

Debugging uploads ​

bash
gform run <form> --auth --policy demo
gform run <form> -r 3 -C 1 --auth -d --pw-trace
gform run last
gform run view <id>          # Content section + status uploaded|assisted|skipped

Init / edit ​

  • gform init stubs file questions with commented source: examples.
  • gform edit <form> --ask → Edit source (local | gdrive | remote | assist) + accept + maxBytes.
  • Prefer asking humans whether they want auto-upload vs assist before inventing a path.