Save a webhook submission as a JSON file in Google Drive
Quick answer: Validate an authenticated webhook’s name, email and request, save them as JSON and upload the file to Google Drive. Sequential production tests passed with documented Unicode name normalisation.
Status: production-tested for sequential requests. Results: 48/48 with documented Unicode name normalisation; 47 of 48 matched exactly.
Receive an authenticated JSON webhook, check name, email and request, write those fields to a local JSON file, then upload it into your chosen Google Drive folder. The graph uses file and JSON components without an AI model.
By Ai Flowi · Published · Updated . Tested with AI Flowi Workflow 1.9.2.
In unicode-literal, HTTP 200 accompanied one new file. Its saved name used Unicode NFD form: the accented letter was decomposed, and the value was identical after NFC normalisation. The email and request fields matched exactly, including Windows CR+LF in the request; the emoji in the name was retained. This is a qualified 48/48 result, not 48 exact matches.
Rejected fields and malformed JSON produced HTTP 422 with no new file observed. Requests without a valid Platform API Key produced HTTP 403 with no new file observed. Valid sequential submissions produced HTTP 200 and a new file each time, including repeat delivery.
Task
Keep an accepted submission as a JSON object in Drive with only name, email and request. Extra input fields and the validation flag do not enter the saved payload. This recipe creates separate Drive files even when the configured filename stays the same.
Related: Amazon S3 Integration.
Who it is for
Teams that need JSON request records in a Drive folder, with a trusted server that can send submissions one at a time. Put webhook authentication in that server integration; embedding a Platform API Key in a public form would expose it.
Sample input
{"name":"Demo Contact","email":"demo@example.com","request":"Keep this request as JSON."}
With the illustrative filename submission.json, the downloaded file should parse to:
{
"name": "Demo Contact",
"email": "demo@example.com",
"request": "Keep this request as JSON."
}
Whitespace and JSON escape formatting are not the test criterion; parsed field values are. The documented NFD name conversion applies to the Unicode case.
Setup
- Authorise a Google connection for Drive uploads and choose a destination folder you can write to. Sheets access alone may lack the required Drive scope. See Google connection setup.
- Confirm that the executing worker has a writable current directory and local backend storage. Write File and Upload File must access the same local file on that worker. This path has not been verified with S3 storage or separate workers.
- Build nine nodes: Universal Webhook, validation JSON Operations, Verification Router, payload JSON Operations, Write File, Google Drive Upload File, response JSON Operations, and success and rejection Webhook Response nodes.
- On Universal Webhook, select
POST,Platform API Keyauthentication andFrom FlowPOST Response Mode. On each JSON Operations node, selectJQ Expressionand use the corresponding query below. - Set Write File to
Localstorage,jsonformat and append mode off. Use a basename without a slash, such assubmission.json. On Upload File, configure the nativefile_pathfield to that same worker file,drive_file_nameto your chosen JSON filename,mime_typetoapplication/json, andparent_folder_idto your destination folder. Select the authorised Google Account. - Connect Write File’s
messageto Upload File’sdescriptionso upload waits for the write. The nativefile_pathhas no connectable Data/JSON/Message port; it needs an executable file path, not just a displayed filename. Verify the configuration with synthetic data before relying on it. - Configure the success response as
200 OKand the rejection response as422 Unprocessable Entity, both withapplication/jsoncontent type. Wait for a submission to finish before sending the next one.
Node connections and field mappings
The payload projection removes verified and keeps only the accepted fields. Write File’s completion message controls execution order through the upload description; the upload reads its separately configured local file path.
| Output | Input |
|---|---|
Universal Webhook request_data | Validation JSON Operations data |
Validation JSON Operations data_output | Verification Router verification_result |
Verification Router verified | Payload JSON Operations data |
Payload JSON Operations data_output | Write File input |
Write File message | Upload File description |
Upload File file | Response JSON Operations data |
Response JSON Operations data_output | Success Webhook Response body_data |
Verification Router rejected | Rejection Webhook Response body_data |
Payload JQ:
{name:.name,email:.email,request:.request}
Response JQ checks for a nonempty upload ID and JSON MIME type. The tested response deliberately returns a literal redacted marker, rather than the usable Drive file ID:
if (.id|type) == "string" then
if (.id|length) > 0 and .mimeType == "application/json" then
{status:"created",file_id:"<redacted-file-id>"}
else error("File upload incomplete") end
else error("File upload incomplete") end
Sanitized request and response examples
Replace the URL and credential placeholders in your trusted sender:
POST https://workflow.aiflowi.com/api/v1/webhook/<your-flow-id>
Content-Type: application/json
x-api-key: <your-platform-api-key>
{"name":"Demo Contact","email":"demo@example.com","request":"Keep this request as JSON."}
HTTP 200 success body for the tested response mapping:
{"status":"created","file_id":"<redacted-file-id>"}
The marker is a literal value in this recipe. It cannot locate the uploaded file. Check the chosen Drive folder and read back the JSON to verify storage.
Omitting email takes the HTTP 422 rejection branch:
{"verified":false,"error":"Validation failed","details":["email must be a valid address up to 254 characters"]}
Validation and failure behaviour
Open the validation JQ
Place this in the validation JSON Operations node with JQ Expression selected.
(.body) as $b
| if ($b|type) != "object" then {verified:false, error:"Request body must be a JSON object with name, email and request."}
else ([
(if ($b.name|type) == "string" then
if ($b.name|length) >= 1 and ($b.name|length) <= 100 and ($b.name|test("[^\\s]")) and (($b.name|test("^\\s|\\s$"))|not) and ($b.name|explode|all(. >= 32 and . != 127)) then empty else "name must be 1-100 characters, nonblank, without control characters or outer whitespace" end
else "name must be 1-100 characters, nonblank, without control characters or outer whitespace" end),
(if ($b.email|type) == "string" then
if ($b.email|length) <= 254 and ($b.email|test("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) and ($b.email|explode|all(. >= 32 and . != 127)) then empty else "email must be a valid address up to 254 characters" end
else "email must be a valid address up to 254 characters" end),
(if ($b.request|type) == "string" then
if ($b.request|length) >= 1 and ($b.request|length) <= 5000 and ($b.request|test("[^\\s]")) and ($b.request|explode|all(. == 9 or . == 10 or . == 13 or (. >= 32 and . != 127))) then empty else "request must be 1-5000 characters, nonblank, with only tab/newline/CR controls" end
else "request must be 1-5000 characters, nonblank, with only tab/newline/CR controls" end)
]) as $errors
| if ($errors|length) > 0 then {verified:false, error:"Validation failed", details:$errors}
else {verified:true, name:$b.name, email:$b.email, request:$b.request} end
endnamemust be a string of 1–100 Unicode code points, contain non-whitespace text and have no outer whitespace, C0 controls or DEL.emailmust be a string up to 254 code points matching the basic address pattern in the query, with no C0 controls or DEL. This checks syntax only; it does not prove deliverability.requestmust be a string of 1–5000 code points containing non-whitespace text. Tab, LF and CR are allowed; other C0 controls and DEL are refused.- The validator does not trim or rewrite accepted values. Read-back found the Unicode name in NFD form, equal after NFC normalisation, while email and request remained exact. The request’s Windows CR+LF was preserved.
- Non-object bodies and rejected fields returned 422, with zero new files observed. The platform returned 403 for missing and invalid keys before the writing branch.
malformed-jsonexpected either 400 or 422; the actual response was 422 with zero new files observed. A different parser deployment is not covered by that observation.- Storage or Google errors have no in-flow catch in this graph. They can stop execution and expose a raw platform/provider error to the webhook caller. Your sender needs a safe user-facing failure response.
A local write can succeed before the Drive upload fails. An upload can also succeed before the webhook response fails, leaving an uncertain outcome. Automatic retry can then create another file. The write-completion message used as the Drive description may contain a worker path; account for that metadata when sharing files.
Test results
The evidence covers synthetic test requests in Ai Flowi’s own workspace on 9 October 2026. The main run contains 48 cases, checking HTTP responses, new-file counts and downloaded JSON field values. It records 47 PASS verdicts and one FAIL: unicode-literal failed exact name comparison despite returning 200 and creating one file.
The latest diagnostic rerun of unicode-literal confirmed NFD in name and equality after NFC normalisation, with exact email and request values. The emoji was retained, and Windows CR+LF was not converted to LF. Both evidence files retain their original exact-comparison FAIL; the documented normalisation supports 48/48 only under that qualified comparison.
Scroll a table horizontally if it needs more room. Its labelled region is keyboard-focusable.
| Case ID | Expected outcome | Observed HTTP | New files observed | Recorded verdict / qualification |
|---|---|---|---|---|
valid | 200; one JSON file, exact fields; extras omitted | 200 | 1 | PASS |
missing-name | 422; no new file | 422 | 0 | PASS |
missing-email | 422; no new file | 422 | 0 | PASS |
missing-request | 422; no new file | 422 | 0 | PASS |
type-name-0 | 422; no new file | 422 | 0 | PASS |
type-name-1 | 422; no new file | 422 | 0 | PASS |
type-name-2 | 422; no new file | 422 | 0 | PASS |
type-name-3 | 422; no new file | 422 | 0 | PASS |
type-name-4 | 422; no new file | 422 | 0 | PASS |
type-email-0 | 422; no new file | 422 | 0 | PASS |
type-email-1 | 422; no new file | 422 | 0 | PASS |
type-email-2 | 422; no new file | 422 | 0 | PASS |
type-email-3 | 422; no new file | 422 | 0 | PASS |
type-email-4 | 422; no new file | 422 | 0 | PASS |
type-request-0 | 422; no new file | 422 | 0 | PASS |
type-request-1 | 422; no new file | 422 | 0 | PASS |
type-request-2 | 422; no new file | 422 | 0 | PASS |
type-request-3 | 422; no new file | 422 | 0 | PASS |
type-request-4 | 422; no new file | 422 | 0 | PASS |
invalid-email | 422; no new file | 422 | 0 | PASS |
malformed-json | 400 or 422; no new file | 422 | 0 | PASS |
missing-key | 403; no new file | 403 | 0 | PASS |
invalid-key | 403; no new file | 403 | 0 | PASS |
blank-name-0 | 422; no new file | 422 | 0 | PASS |
blank-name-1 | 422; no new file | 422 | 0 | PASS |
blank-email-0 | 422; no new file | 422 | 0 | PASS |
blank-email-1 | 422; no new file | 422 | 0 | PASS |
blank-request-0 | 422; no new file | 422 | 0 | PASS |
blank-request-1 | 422; no new file | 422 | 0 | PASS |
non-object-0 | 422; no new file | 422 | 0 | PASS |
non-object-1 | 422; no new file | 422 | 0 | PASS |
non-object-2 | 422; no new file | 422 | 0 | PASS |
non-object-3 | 422; no new file | 422 | 0 | PASS |
name-too-long | 422; no new file | 422 | 0 | PASS |
email-too-long | 422; no new file | 422 | 0 | PASS |
request-too-long | 422; no new file | 422 | 0 | PASS |
name-control | 422; no new file | 422 | 0 | PASS |
request-control | 422; no new file | 422 | 0 | PASS |
name-leading-space | 422; no new file | 422 | 0 | PASS |
name-trailing-space | 422; no new file | 422 | 0 | PASS |
unicode-literal | 200; one JSON file, exact fields; extras omitted | 200 | 1 | Original: FAIL; passes after NFC name normalisation in diagnostic rerun |
minimum-boundaries | 200; one JSON file, exact fields; extras omitted | 200 | 1 | PASS |
boundaries | 200; one JSON file, exact fields; extras omitted | 200 | 1 | PASS |
unicode-name-boundary | 200; one JSON file, exact fields; extras omitted | 200 | 1 | PASS |
unicode-name-too-long | 422; no new file | 422 | 0 | PASS |
extra-fields | 200; one JSON file, exact fields; extras omitted | 200 | 1 | PASS |
repeat-first | 200; one JSON file, exact fields; extras omitted; a separate file on each delivery | 200 | 1 | PASS |
repeat-second | 200; one JSON file, exact fields; extras omitted; a separate file on each delivery | 200 | 1 | PASS |
repeat-first and repeat-second each returned 200 and added one file. The second file had a distinct ID, so repeating a submission does not deduplicate it.
Delivery format: Tested recipe
Recreate this graph with the settings, ports and JQ provided here. An importable workflow download is not supplied. Runtime observations establish the tested sequential path, but do not establish import round-trip correctness or editor auto-save persistence.
Required configuration
You need a Drive-authorised Google Account, a writable destination folder, local backend storage on the executing worker, and matching Write File and Upload File paths. Configure your webhook Production Endpoint for POST, Platform API Key authentication and From Flow responses.
Find Write File, Google Drive Upload File and the processing nodes in the available component catalog. A Google connection is required for uploads; no model credential is configured in this graph.
Related: Google Drive integration for AI Flowi Workflow.
Tested version and date
The recipe was exercised on against AI Flowi Workflow 1.9.2. The supplied evidence includes the full 48-case run and a diagnostic rerun for the Unicode mismatch. It does not cover concurrent delivery, import or editor persistence.
Usage and cost assumptions
Plans: Go from USD8/month billed yearly (USD10 monthly), Plus from USD16 (USD20 monthly), Pro 100 from USD80 (USD100 monthly), Pro 200 from USD160 (USD200 monthly) and Pro 500 from USD400 (USD500 monthly), with 5,000 to 250,000 Flowi Tokens/month and top-ups; 1 token per node run; third-party API and model usage is not included.
These tests measure responses and stored files, not billing. Inspect transaction logs for executed nodes, retries and failed-run charges before budgeting. This graph has no AI model configured. Google connection and local storage requirements still apply. Third-party API/model usage is excluded from the plan.
Limitations
- The fixed local staging file is sequential-only. Concurrent submissions can overwrite the payload before upload. Supporting concurrency requires a per-request server path and an upload component that can receive that path through an edge.
- Repeated deliveries create additional Drive files with the same configured filename. There is no duplicate protection, rollback or automatic recovery of uncertain outcomes.
- Write File needs a writable worker current directory and local backend storage accessible by Upload File. S3 storage and execution across separate workers were not verified.
- Write File also creates a platform copy. Local staging and platform copies are retained, so configure retention and access for those copies as well as the Drive file.
- Authorise the necessary Drive scope even if Sheets already works. Provider failures can reach webhook callers as raw errors; the graph has no in-flow Google exception catch.
- The tested Unicode name was stored in NFD form. Compare after NFC normalisation where appropriate; do not assume exact code-point identity for accented names.
- Drive listings are eventually consistent. Zero new files in reject observations is not an atomic no-write guarantee.
- The tested response’s
file_idis a redacted marker, not a usable identifier. No import round-trip or editor auto-save check supports a downloadable template claim.
Next steps
Start with your own synthetic submission and inspect the saved JSON, destination folder and transaction logs. Keep delivery sequential and let the sender handle failures safely before collecting real requests. The webhook-to-Google-Sheets tutorial provides related validation and Google connection guidance.