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

  1. 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.
  2. 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.
  3. 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.
  4. On Universal Webhook, select POST, Platform API Key authentication and From Flow POST Response Mode. On each JSON Operations node, select JQ Expression and use the corresponding query below.
  5. Set Write File to Local storage, json format and append mode off. Use a basename without a slash, such as submission.json. On Upload File, configure the native file_path field to that same worker file, drive_file_name to your chosen JSON filename, mime_type to application/json, and parent_folder_id to your destination folder. Select the authorised Google Account.
  6. Connect Write File’s message to Upload File’s description so upload waits for the write. The native file_path has 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.
  7. Configure the success response as 200 OK and the rejection response as 422 Unprocessable Entity, both with application/json content 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.

Eight graph connections
OutputInput
Universal Webhook request_dataValidation JSON Operations data
Validation JSON Operations data_outputVerification Router verification_result
Verification Router verifiedPayload JSON Operations data
Payload JSON Operations data_outputWrite File input
Write File messageUpload File description
Upload File fileResponse JSON Operations data
Response JSON Operations data_outputSuccess Webhook Response body_data
Verification Router rejectedRejection 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
  end
  • name must be a string of 1–100 Unicode code points, contain non-whitespace text and have no outer whitespace, C0 controls or DEL.
  • email must 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.
  • request must 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-json expected 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.

All cases, including the original Unicode comparison failure
Case IDExpected outcomeObserved HTTPNew files observedRecorded verdict / qualification
valid200; one JSON file, exact fields; extras omitted2001PASS
missing-name422; no new file4220PASS
missing-email422; no new file4220PASS
missing-request422; no new file4220PASS
type-name-0422; no new file4220PASS
type-name-1422; no new file4220PASS
type-name-2422; no new file4220PASS
type-name-3422; no new file4220PASS
type-name-4422; no new file4220PASS
type-email-0422; no new file4220PASS
type-email-1422; no new file4220PASS
type-email-2422; no new file4220PASS
type-email-3422; no new file4220PASS
type-email-4422; no new file4220PASS
type-request-0422; no new file4220PASS
type-request-1422; no new file4220PASS
type-request-2422; no new file4220PASS
type-request-3422; no new file4220PASS
type-request-4422; no new file4220PASS
invalid-email422; no new file4220PASS
malformed-json400 or 422; no new file4220PASS
missing-key403; no new file4030PASS
invalid-key403; no new file4030PASS
blank-name-0422; no new file4220PASS
blank-name-1422; no new file4220PASS
blank-email-0422; no new file4220PASS
blank-email-1422; no new file4220PASS
blank-request-0422; no new file4220PASS
blank-request-1422; no new file4220PASS
non-object-0422; no new file4220PASS
non-object-1422; no new file4220PASS
non-object-2422; no new file4220PASS
non-object-3422; no new file4220PASS
name-too-long422; no new file4220PASS
email-too-long422; no new file4220PASS
request-too-long422; no new file4220PASS
name-control422; no new file4220PASS
request-control422; no new file4220PASS
name-leading-space422; no new file4220PASS
name-trailing-space422; no new file4220PASS
unicode-literal200; one JSON file, exact fields; extras omitted2001Original: FAIL; passes after NFC name normalisation in diagnostic rerun
minimum-boundaries200; one JSON file, exact fields; extras omitted2001PASS
boundaries200; one JSON file, exact fields; extras omitted2001PASS
unicode-name-boundary200; one JSON file, exact fields; extras omitted2001PASS
unicode-name-too-long422; no new file4220PASS
extra-fields200; one JSON file, exact fields; extras omitted2001PASS
repeat-first200; one JSON file, exact fields; extras omitted; a separate file on each delivery2001PASS
repeat-second200; one JSON file, exact fields; extras omitted; a separate file on each delivery2001PASS

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_id is 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.