File store for functions: save files and share pre-signed URLs
Give Webhook Relay functions persistent file storage with require("files:name"): write reports and exports, read files between runs, list folders and hand out pre-signed download or upload URLs, in JavaScript or Lua.
A Webhook Relay function can save and read files with require("files:name"), a persistent file store with folders and pre-signed URLs. Use it to archive webhook payloads, build a daily CSV, keep a template the function reads on every run, or hand someone a download link, without setting up your own bucket. The store is created automatically the first time a function imports it.
const files = require("files:exports")
// Write a file; the content type is optional
files.put("reports/today.csv", "id,total\n1,49.00\n", "text/csv")
// Read it back as text and inspect its metadata
const csv = files.get("reports/today.csv")
const info = files.stat("reports/today.csv") // { path, size, content_type, modified_at }
// List a folder, then share a download link (valid 30 minutes by default)
const listing = files.list("reports/", 100) // { files: [...], cursor: "..." }
const link = files.url("reports/today.csv")
files.delete("reports/today.csv")
How stores are created and shared
The name after files: is the store. The first time a function runs require("files:exports"), Webhook Relay creates an empty file store called exports in your account and binds it to that function. Other functions that require files:exports share it, so one function can write a file that another reads.
Store names start with a letter, contain only letters, numbers, _ or -, and are at most 64 characters. Paths inside a store can contain /, which the dashboard and list() treat as folders.
In the dashboard, Functions → Files Store lets you browse folders, upload and download files, and delete files or whole stores. Deleting a store works as a reset. The next run that requires the name gets a fresh, empty store.
API
All methods are synchronous: they return the result directly, with no await or callback.
| Method | Returns | Notes |
|---|---|---|
files.put(path, content[, contentType]) | true | Writes text content, replacing any existing file. Up to 32 MiB |
files.get(path) | string | Reads the file as text. Up to 8 MiB |
files.getBuffer(path) | base64 string | Reads binary content. Up to 8 MiB |
files.exists(path) | boolean | |
files.stat(path) | { path, size, content_type, modified_at } | |
files.list([prefix[, limit[, cursor]]]) | { files, cursor } | Each entry has path, size and modified_at. 100 per page by default, up to 1,000 |
files.delete(path) | true | |
files.url(path[, method[, ttl]]) | string | Pre-signed URL. method defaults to GET; ttl in seconds defaults to 30 minutes, up to 7 days |
A missing file, an oversized read or a storage failure is thrown as an exception with a short message.
Archive webhook payloads
Keep a copy of every event, one file per day, alongside the normal delivery:
const files = require("files:archive")
const event = JSON.parse(r.body)
const day = new Date().toISOString().slice(0, 10) // 2026-10-28
files.put("stripe/" + day + "/" + event.id + ".json", r.body, "application/json")
The request is forwarded as usual. The archive is a side effect you can list later with files.list("stripe/2026-10-28/"). For high volume or long retention, a GCS or S3 service connection writes straight to your own bucket instead.
Build a report and share a link
Append rows during the day and send a link when a scheduled webhook fires:
const files = require("files:reports")
const path = "orders/" + new Date().toISOString().slice(0, 10) + ".csv"
const existing = files.exists(path) ? files.get(path) : "id,total\n"
const order = JSON.parse(r.body)
files.put(path, existing + order.id + "," + order.total + "\n", "text/csv")
// Later, from the scheduled run: a link that works for 24 hours
// const link = files.url(path, "GET", 24 * 3600)
Read-modify-write is not atomic, so two webhooks arriving at the same moment can overwrite each other's row. For an exact ledger, write one file per event and combine them when you build the report.
Large files and uploads
Function code works with files up to 32 MiB on write and 8 MiB on read. For anything bigger, or for binary uploads such as images and PDFs, create a pre-signed URL and transfer the bytes directly:
const files = require("files:uploads")
const uploadUrl = files.url("invoices/inv_123.pdf", "PUT", 600) // valid for 10 minutes
r.setResponseBody(JSON.stringify({ upload_url: uploadUrl }))
The client then PUTs the file to that URL, and later runs read its metadata with files.stat() or share it with files.url().
Limits
| Limit | Value |
|---|---|
| Single write from a function | 32 MiB |
Single read (get / getBuffer) | 8 MiB |
| File path | 1,024 bytes |
list() page size | 100 by default, up to 1,000 |
| Pre-signed URL lifetime | 30 minutes by default, up to 7 days |
| Stores per account | 50 (KV and file stores combined) |
For small structured values, such as IDs, tokens, counters and cursors, the KV store is simpler and supports TTL.
Manage files from the API, MCP or an agent
File stores are also available through the REST API under /v1/function-stores (list, upload, download, stat, delete and pre-signed URLs) and through the Webhook Relay MCP server. The MCP tools include list_function_store_files, read_function_store_file, write_function_store_file, get_function_store_file_url and create_function_store_upload_url.
Migrating from Pipedream File Stores
Pipedream steps stage files on /tmp, which Webhook Relay's sandbox does not have. When porting, replace $.files.open(path).toFile("/tmp/x") with files.get(path) or files.getBuffer(path), and pass bytes or a files.url() link to the next step instead of a local path. The Pipedream Workflows migration guide covers the rest of the mapping.
Frequently asked questions
Can Webhook Relay functions store files?
Yes. A function calls require("files:name") and gets a persistent file store with put, get, getBuffer, exists, stat, list, delete and url. Files persist between runs, can be organised in folders by path, and can be shared with pre-signed download or upload URLs.
Do I need to create the file store first?
No. The first time a function runs require("files:exports"), Webhook Relay creates an empty file store named exports and binds it to that function. Other functions that require the same name share it. You can browse, upload, download and delete files in the dashboard under Functions, Files Store.
How large can files be?
A function can write up to 32 MiB in one put call and read up to 8 MiB with get or getBuffer. For larger objects, generate a pre-signed URL with files.url(path, "PUT") and upload or download directly against storage.
How do I share a file from a function?
Call files.url(path) to get a pre-signed download link, valid for 30 minutes by default. Pass a method and a lifetime in seconds to change it, for example files.url(path, "PUT", 600) for a ten-minute upload link. Links can last up to seven days.
Can I write binary files?
Function code writes text content with put, and reads binary files back as base64 with getBuffer. For binary uploads such as images or PDFs, create a pre-signed PUT URL and upload the bytes directly, or upload them in the dashboard.
