All topics

Saving data

How a drop keeps data: per visitor in the browser, as submissions the maker collects, or as shared values every visitor sees, plus the exact functions, the Submissions page, Google Sheets, forwarding submissions to a webhook and the limits.

Last updated

Three ways a drop can keep data

  1. Private per visitor. Each visitor keeps their own data on their own device; nobody else sees it. This is plain localStorage (or sessionStorage). Nothing reaches Toolcrow and nothing shows up on the Submissions page.
  2. Collected submissions. Every entry (a form, a vote, a sign-up) becomes a row that the maker sees on the Submissions page and can export. This is Tooldrop.opslaan().
  3. Shared for everyone. One shared state that all visitors see and update, like a shared list, settings or a counter. This is window.storage with shared set to true.

A drop can combine them, for example collect submissions and keep a visitor's draft in localStorage. Which one fits depends on what the maker wants visitors to see: Toolcrow never switches a tool from private to collected or shared data on its own.

Private per visitor: localStorage and sessionStorage

  • Inside a drop, localStorage and sessionStorage work as usual, but Toolcrow backs them with the browser's IndexedDB. Data survives refreshes and is not limited to the usual ~5 MB of browser storage.
  • sessionStorage is kept as well and is shared between tabs, so it behaves like localStorage.
  • The data is stored per drop (each drop has its own address) and per device. Values are stored as text.

Submissions: Tooldrop.opslaan and Tooldrop.lezen

  • Toolcrow adds Tooldrop.opslaan to every drop, but the drop's own code has to call it, for example when the form is sent. A tool built elsewhere often doesn't. Then what people fill in reaches nobody: the Submissions page stays empty and there is nothing to send to a sheet or another service. When the scan sees a form but the tool keeps nothing at all (no Tooldrop.opslaan(), no shared values, no database of its own and nothing in the visitor's browser), the drop page (Needs you and the Connections panel) and Nest say "Your tool has a form, but doesn't save what people send", with a ready-made instruction for the AI behind "Send to your AI" (open it in Claude or ChatGPT, or copy it; "See prompt" shows the text first); a connected AI can propose the change as a new version for the maker to check. Toolcrow never changes the code itself. For a form that only calculates or looks something up, the maker can click "This form doesn't need saving".
  • Tooldrop.opslaan(data) saves one submission. data is a plain object; flat text or number values work best (nested objects and lists are stored as JSON text). It returns a promise of { gelukt: true } or { gelukt: false, reden }.
  • The keys of data are the field names. They become the column names on the Submissions page, the export and a connected Sheet, exactly as written. Any non-empty text works, including spaces, accents and symbols such as €. Names are case-sensitive: Name and name are two columns. submittedAt and Submitted at are used for the time of saving; a field with one of those names can get lost or overwrite that time.
  • Tooldrop.lezen() returns a promise of { gelukt: true, rijen: [...] }, or { gelukt: false, reden, rijen: [] }. Each row is an object with a submittedAt field plus the saved fields, in the order they were saved. submittedAt is the time of saving in UTC, as ISO text such as 2026-10-15T09:30:00.000Z. All values come back as text (numbers and objects included), the same way a spreadsheet cell does.
  • In the built-in database a row only has the fields that this submission saved. With a connected Sheet every row has every column, with an empty text where a submission had no value.
  • Reading submissions back only works when the maker has turned on "Visitors can read submissions" in the Sharing window (under "For visitors") or on the Access tab. It is off by default for privacy, and turning it on asks the maker to confirm first, because everyone who can open the drop can then read every submission; while it is off, lezen() answers { gelukt: false, reden: "niet-publiek", rijen: [] }. Saving always works.
  • While it is on, every visitor can read every field of every submission, including fields the tool itself does not show. With a connected Sheet this is the whole first tab, including columns the maker adds there by hand.
  • Tooldrop.context holds { dropId, titel, access } for the current drop. access is "open", "password" or "private": how the drop is shared right now. A tool can use it to decide what to keep in shared storage, for example to share data only while the drop is behind a password or private. Outside Toolcrow (a downloaded file, an AI chat preview) there is no Tooldrop.context.

Shared values: window.storage

window.storage is an asynchronous key-value store. Its signature matches the storage API of Claude artifacts, so an artifact built in Claude keeps working on Toolcrow.

  • window.storage.get(key, shared) returns { key, value, shared }, or null when the key does not exist.
  • window.storage.set(key, value, shared) returns true or false.
  • window.storage.delete(key, shared) returns true or false.
  • window.storage.list(prefix, shared) returns an array of keys.
  • Only when the last argument is exactly true is the value shared between all visitors. Without it, the value is stored on the visitor's own device only.
  • Values are text. Objects and lists are stored with JSON.stringify and read back with JSON.parse.
  • Shared keys are at most 200 characters, without spaces, slashes or quotes. These rules apply to shared keys only; keys kept on the visitor's device have no rules.
  • Shared values can be read and changed by everyone who can open the drop. The "Visitors can read submissions" switch does not apply to them.
  • A shared get also answers null when reading fails, the same as for a key that does not exist. To tell the two apart, a tool can first read a key it knows exists; a tool that reads a value, changes it and writes it back then never replaces shared data it could not read.
  • If a drop defines its own window.storage, Toolcrow leaves it alone.

Failing softly

None of these functions throw. Tooldrop calls answer with { gelukt: false, reden }; window.storage answers false or null. Common reden values:

  • vol: the store for this drop is full (see limits below). Toolcrow tells the maker (see "When the built-in database is full").
  • te-groot: one value or submission is too large.
  • drop-limiet: the daily number of storage calls for this drop is used up; it resets the next day.
  • niet-publiek: reading submissions is not allowed (see above).
  • geen-data / ongeldige-data: the submission is not a plain object (for example a list or a text), or it cannot be turned into JSON.
  • ongeldige-sleutel: the key breaks the key rules.
  • niet-verbonden: the connected Google Sheet is no longer reachable (the maker needs to reconnect Google).
  • schrijffout / leesfout: saving to or reading from the store failed. With a connected Google Sheet this usually means Google refused: the Sheet was deleted or moved to the bin, it is no longer shared with the connected Google account, or Toolcrow was connected without permission to use files (when Google asks, the box for files used with this app has to be ticked). It can also be a short hiccup at Google. When the Sheet is gone or can no longer be edited, Toolcrow notices it by itself and tells the maker (see "Where the data is stored"). The maker can open the Sheet to check it still exists, and reconnect Google from the drop page (Reconnect under Connections) or the Google Sheets page under Connections. Without a connected Sheet, it is a problem on Toolcrow's side; trying again later usually works.
  • unauthorized: the page's access has run out or changed since it was opened: it has been open for a long time, or the maker has since made the drop private, changed its password or turned it off. Reloading the page fixes it, as long as the visitor still has access.
  • netwerk / niet-beschikbaar: no connection to Toolcrow, or the page is not running as a drop (for example in an AI chat preview).

When the functions are there

Toolcrow sets up Tooldrop, window.storage, window.claude and the storage behind localStorage before any of the page's own code runs, so a script can use them from its first line, without waiting for a load event. In a ZIP upload this holds for the start page; other HTML pages in the ZIP are served as plain files, without these functions.

Outside Toolcrow, for example in the preview inside ChatGPT or Claude, Tooldrop does not exist. Code that checks for it and falls back to localStorage keeps working in both places.

Where the data is stored

  • By default, submissions and shared values are stored in Toolcrow's built-in database (in the EU), separately for each drop.
  • The maker can connect a Google Sheet to a drop. They connect a Google account once (on the Google Sheets page under Connections); every drop can then use that connection without signing in again, and each drop picks its own Sheet. On the drop page, Connections → "Add a connector" → Google Sheets lists the maker's Google accounts (or "Sign in with Google" if there is none yet); picking one leads to "Create a new Google Sheet" (named "Submissions · <drop name>") or "Choose an existing sheet", and the last step has "Open in Google Sheets". Toolcrow only gets access to the files it creates or the maker picks, not the whole Drive. From then on the same calls write to that Sheet instead: submissions go to the first tab (column A is "Submitted at", a column is added for each new field) and shared values to a tab named toolcrow_data (key, value, updated). The drop's code stays the same.
  • "Stop using it in this drop" in the drop's Google panel only takes that drop off Google; the connection stays for the other drops. Deleting the connection on Connections gives the access back to Google straight away, and every drop that used it stores new data in the built-in database again. The rows already in the Sheets stay in the maker's Google Drive.
  • "Submissions saved" in the Usage panel on the drop page counts only what is in the built-in database, not the rows in a connected Sheet.
  • A connected Sheet always wins. If Google cannot be reached, the call fails softly; it does not fall back to the built-in database.
  • If Google stops Toolcrow's access to the Sheet (someone removed Toolcrow from the Google account, the password changed, or Google ended the connection), calls answer niet-verbonden and nothing is saved until the maker reconnects. Because the connection belongs to the Google account and not to one drop, every drop that uses it stops at once, and reconnecting once fixes them all. Toolcrow first checks with Google that the connection is really broken, then emails the maker (one email per account at most once a day, listing every tool that broke) and shows it as a Problem at the top of Nest and of "Needs you" on the home page, with a "Reconnect Google" button. Toolcrow keeps no copy of what failed in the meantime: those submissions are lost unless the tool kept its own copy. Toolcrow also checks the connected Sheets once a day, so the maker can often reconnect before anyone submits something.
  • If the chosen Sheet itself is no longer usable, Toolcrow tells the maker in the same way: the Sheet was deleted (or the connected Google account can no longer open it), it was moved to the Google Drive trash, or the connected account can only view it, not edit it. Before it says anything, Toolcrow asks Google itself whether the file still exists and can be edited; it asks only that, never for the contents. Google empties its trash after 30 days and saving into a Sheet in the trash often still works until then, so Toolcrow also checks every chosen Sheet once a day, even when nobody uses the drop. The Problem says what happened and what was not saved in the meantime. Its button opens the Connections panel on the drop page to choose another Sheet ("Choose a Sheet"), or the Google Drive trash to restore it ("Open Google Drive trash"). It closes by itself once the Sheet works again, or when another Sheet or Google connection is chosen.
  • Toolcrow puts every value in its column by name, so a value never lands under the wrong heading. If the maker renames or deletes a column, or the tool starts sending a new field, the next submission adds a column with that name. If the column cannot be added (for example because the first row is protected), the value is still saved, as field: value at the end of the row, in a column without a heading.
  • Toolcrow writes every value to the Sheet as plain text, including numbers, dates and "Submitted at". Google does not turn them into numbers or dates, and does not run text that starts with = as a formula. To calculate with a column in the Sheet, it has to be converted first, for example with VALUE().
  • Reading back from a Sheet gives each cell as the Sheet displays it. Values Toolcrow wrote come back unchanged. Cells the maker typed or edited in the Sheet come back in the Sheet's own format: a date as the Sheet shows it (in the Sheet's locale), a number with its formatting, a formula as its result.
  • Rows come back in the order of the Sheet's rows, so sorting the Sheet changes the order lezen() returns.
  • Toolcrow writes to the Sheet with the maker's own Google account. Google does not send notifications about changes made with your own account, so a notification rule the maker sets in the Sheet does not email them about new submissions. To get a message for each new submission, forward submissions to Slack or Discord (below).

Webhooks: sending submissions to other apps

The maker can have every new submission also sent to a webhook: an address from a service such as Slack, Discord, Microsoft Teams, Airtable, a HubSpot form, Zapier, Make or n8n that turns it into an action, like a message in a channel, a new row in a table or a new contact. The drop's code stays the same.

  • The usual way is to ask the AI connected to Toolcrow (see Connecting your own AI), for example "send every new submission of this drop to my Slack channel". The AI creates the webhook in that service if it has access to it (otherwise it explains where to find it) and proposes it to Toolcrow with propose_forwarding. The proposal appears on the drop page (Needs you, and under Connections as "Waiting for you") and in Nest as a request; "Review" opens the panel of that connection, and nothing is forwarded until the maker clicks "Approve…" and then confirms the address with "Approve and start sending". The proposal shows the address (the host, such as hooks.slack.com) first; the name and explanation come from the AI. If the AI replaces the proposal in the meantime, the maker sees the new address before anything is approved. Using a webhook from the maker's own list on a drop also asks the maker to confirm the address. "Decline" removes the proposal. In the drop's Connections panel, "Add a connector" goes step by step: pick a service, connect it (one of the maker's own connections for that service, or, if there is none yet, "Let your AI set it up" with a ready-made instruction for that exact service behind "Send to your AI", or "Add it yourself on Connections"), for Google choose the sheet, and a last step that says where submissions now go, with "Send test" or "Open in Google Sheets" and a link to try the tool. The tool's code does not change: it hands each submission to Toolcrow with Tooldrop.opslaan(), and Toolcrow sends it on with all the form fields, in the form that service expects.
  • The maker can also add a webhook in advance on the Connections page. Under "Your connectors" each service the maker uses has one tile with the number of connections; a red dot means Toolcrow knows something is wrong (the last delivery failed, or Google refused access). Under "Add a connector" each service has a tile that says how it connects ("Webhook": the maker pastes an address from the service; "Sign in": the maker signs in to the service, like Google Sheets); the tile opens the page of that service. "Any webhook" is for a service that is not in the list but can receive a webhook. Each service has its own page (for example /connections/teams) with the maker's connections for that service and a table of what it can do. "Add Teams" opens a side panel with the setup steps, the fields (webhook URL and an optional name) and a button that copies a prompt for the AI; once there is a connection, the button reads "Add another". How the connection keeps the address safe is behind the "?" next to "Secret address" at the top. A connection added there is not used by any drop yet: the AI sees it through list_connections and proposes it for a drop, or the maker picks it on the drop page (Connections → "Add a connector"). A new connection is only added on the Connections page, never by pasting an address on a drop page. Clicking a connection opens a side panel with Details (where it sends to, how it connects, when it was added), a Used by table (each drop and whether it sends or waits for approval; for Google, which sheet each drop saves to), an Activity table of every delivery in the last 30 days (when, drop, delivered or failed and why, never the content), and Delete (only on the Connections page; on a drop page the panel links to it instead, because deleting stops every drop that uses the connection). A Google connection only shows Reconnect when Google refused access. Opened from a drop page, the same panel starts with "In this drop": what that drop does with the connection (Send test and "Stop using it in this drop" for a webhook; the Sheet it saves to, Change file and "Stop using it in this drop" for Google; Approve or Decline for a proposal from the AI). Toolcrow checks that an address matches the page (a Discord address on the Teams page is refused); "Any webhook" accepts any address.
  • Only submissions are forwarded (Tooldrop.opslaan()), not shared values from window.storage. A drop that never calls Tooldrop.opslaan() forwards nothing.
  • Where submissions go. Without anything connected, Toolcrow keeps every submission (the Submissions page). A chosen Google Sheet replaces that: submissions go only to the Sheet. A webhook comes on top: Toolcrow keeps the submission and also sends it on. The drop page shows this under "Where submissions go".
  • Keep a copy in Toolcrow. When a drop forwards to a webhook and has no Google Sheet, the maker can turn off the copy Toolcrow keeps. The copy is on by default and handy as a backup, but not required. With the copy off, new submissions only go to the webhook and do not show up on the Submissions page, and the tool cannot read them back with Tooldrop.lezen(). Toolcrow still keeps any submission it could not send (the daily limit was reached, the address was refused or the delivery failed), so nothing is lost. Without an approved webhook the switch has no effect and Toolcrow keeps everything.
  • Forwarding comes on top of saving. The submission is still stored in the built-in database or the connected Sheet, and it is forwarded even if saving failed. A submission refused by the daily storage call limit is not forwarded.
  • Toolcrow recognises the service from the address. Slack and Discord get a short message listing the fields, with a link to the drop's Submissions page; mentions such as @everyone are switched off. Zapier, Make, n8n and any other address get JSON: { "event": "submission", "tool": { "name": …, "url": … }, "submitted_at": …, "data": { … } }, with data holding the fields exactly as the tool saved them. Slack workflow webhooks (hooks.slack.com/triggers/…) get the fields as flat text values.
  • Microsoft Teams: the address comes from the Teams workflow "Send webhook alerts to a channel" (the older Office 365 connectors no longer work). Teams gets a card with the tool name, the fields and a "View in Toolcrow" button. Teams accepts at most 28 KB per message, so very long submissions are shortened.
  • Airtable: the address comes from an automation that starts with "When webhook received". Airtable gets one flat object: { "event": "submission", "tool": …, "tool_url": …, "submitted_at": …, <your fields> }. After the first message (use "Send test"), the maker adds a "Create record" step in Airtable and picks which field goes where. Each message counts as an automation run in Airtable.
  • HubSpot form: the address is https://api.hsforms.com/submissions/v3/integration/submit/<portalId>/<formId> (both IDs are in the form's embed code; no key needed). Each submission becomes a form submission in HubSpot, and so a contact. The drop's field names must match the form's internal field names (for example email, firstname, company): HubSpot refuses the whole submission when a field is not on the form, which shows as HTTP 400 under "Activity" in the webhook's panel. The drop's name and address are sent as the page it came from. A form with reCAPTCHA turned on refuses submissions sent this way. "Send test" creates a test contact with the address test@toolcrow.app.
  • "Send test" sends one message with "event": "test" and the field names of the latest saved submission (with the value "example"), so a Zap or scenario can be set up before the first real submission.
  • The address is stored encrypted on Toolcrow's side and never shown again in full, only its domain. Visitors never see it. Toolcrow sends from its own server after answering the visitor, so the tool does not wait, and the service's answer never reaches the browser: Tooldrop.opslaan() only reports whether saving worked.
  • Toolcrow only sends to public https addresses on the standard port, does not follow redirects, and waits at most 5 seconds for an answer. A message larger than 64 KB is not forwarded.
  • Up to 10 forwards per visitor and 100 per drop per day (Amsterdam time), plus 10 tests per drop per day. This keeps a drop from flooding the maker's channel or using up the task allowance of a service like Zapier. Above it, submissions are still saved, just not forwarded. To count per visitor without tracking anyone, Toolcrow uses a code made from the IP address and the drop that changes every day and cannot be turned back into the address.
  • "Activity" in the panel of a webhook shows its deliveries: when, whether it arrived, and why not (for example HTTP 404 when the webhook was deleted at the service), never the content. Toolcrow keeps this record for 30 days.
  • If messages stop arriving, Toolcrow says so in Nest and under Needs you, with the button "Check webhook". That button opens the webhook's panel on the drop page. One line at the top says what went wrong; "Details" next to it shows why it usually happens and what to do. Connections on the drop page shows "Not arriving" next to the webhook for as long as this lasts. Toolcrow does not email about this, because the submissions themselves are saved; only the message is missing, and Toolcrow does not send a failed message again. It shows up when the webhook no longer exists or no longer accepts messages (two failed deliveries in a row), when the service keeps refusing what the tool sends or has an outage (three in a row, or more than half of at least five in a day, spread over at least 15 minutes, so a short hiccup never shows up), or when the address no longer passes Toolcrow's safety check (two in a row, spread over at least 15 minutes). It closes by itself when a delivery arrives (after a refusal or an outage: three in a row, or a week without a failure), when "Send test" works, or when the maker picks another webhook or stops forwarding.
  • Toolcrow never sends a test message by itself to check a webhook, because it would land in the maker's channel. It only knows from real deliveries, so a webhook that stopped working on a drop nobody uses shows up with the next submissions. Whether the service does something with a message it accepted is something Toolcrow cannot see.
  • When a drop reaches the daily limit of 100 forwards, Nest shows one line that does not count as needing the maker: nothing is lost and forwarding starts again the next day. It closes after a day without hitting the limit. The limit per visitor never shows up: that is the limit doing its work.
  • One webhook per drop. A proposal to switch to another webhook leaves the current one working until the maker approves it. "Stop using it in this drop" in the webhook's panel on the drop page stops forwarding for that drop and keeps the webhook for other drops. Removing a webhook on the Connections page stops forwarding for every drop that used it. Deleting a drop stops its forwarding.

The Submissions page

Each drop has a Submissions page (/drop/<slug>/data, opened with "View submissions" in the Usage panel on the drop page). Only the maker can open it. It shows what the built-in database holds for that drop: a table of submissions and a table of shared keys, with search across all columns, sorting, selecting and deleting rows, adding a row by hand, export to CSV or JSON, and "Clear all data". Rows cannot be edited in place: a submission can only be deleted (and a new one added by hand), and a shared value is changed by adding a row with the same key, which overwrites it. When a Sheet is connected, the data lives in the Sheet. When the page is empty, it shows a ready-made prompt to paste into an AI assistant together with the tool, so the tool starts saving its data to Toolcrow.

Limits per drop

  • 50,000 characters per shared value or per submission.
  • 1,000 shared keys (overwriting an existing key always works).
  • 5,000 submissions.
  • 1,000 storage calls per day, reading and writing together, for submissions and shared values. The day runs on Amsterdam time.
  • 10 forwards per visitor and 100 per drop per day, when forwarding to a webhook is on. The first three apply to the built-in database. With a Google Sheet connected, Google's own limits apply instead, such as 50,000 characters per cell. The daily call limit applies in both cases. Private per-visitor storage has none of these limits.

When the built-in database is full

When a drop's built-in database holds 5,000 submissions (or 1,000 shared keys), new ones are refused with vol; overwriting an existing key still works. The first refusal makes it a Problem in Nest and under "Needs you", with an email in the daily bundle (see Feedback and Nest): it says how many submissions were refused since the store was full. "Make room" opens the Submissions page, where the maker can export what to keep and delete the rest. Connecting a Google Sheet also works: new submissions then go to the Sheet. The Problem closes by itself as soon as there is room again.

Something missing or unclear? Email hello@toolcrow.app. We answer personally.