Submissions
Every accepted submission is stored (unless the form's Store submissions is off) with:
| Column | Content |
|---|---|
number |
The sequential number within the form (#42) |
data |
The values, keyed by field key, normalised by the field type; hidden and conditionally hidden fields as null |
fields |
The label and type of every field at submission time |
user_id |
The logged-in user, when there was one |
ip, user_agent |
Optional (submissions.store_ip, store_user_agent) |
fingerprint |
The visitor's anonymous id, for "one per person" |
source_url |
The page the form was on |
channel |
web, json, livewire or code |
read_at |
Set when someone opens it in the panel |
In the panel
The Submissions relation manager on the form's edit page lists them newest first: the number, the date, a summary, and one column per field (the first two shown, the rest toggleable), each searchable and sortable. Filters: unread, one per field of a choice, boolean or date type, and the share link on a form that has some. Opening one shows every value (copyable; uploaded files as download links; rich text rendered) and the details, and marks it read. Edit changes the values in a slide-over with the same Filament components as the Livewire renderer. Bulk actions mark as read, export or delete.


Export CSV downloads the filtered list: the number, the id, the date, the form's fields (hidden ones included; an address gets a column per part), then any key an older submission still has, then the page, IP and user. Export Excel appears when OpenSpout is installed (Filament's export action brings it; otherwise composer require openspout/openspout).
Once a form has submissions, its edit page opens with Submissions per day, a bar chart over the last 7, 30 or 90 days with the totals (all time, the last 7 days, unread) above it. It counts the submissions table: no tracking script.
The Forms navigation item shows the unread count (navigationBadge(false) to hide it).
Files
Uploads live on uploads.disk (local by default: private) under uploads.directory/{form id}/, named {ulid}__{original-name}.{ext}. The panel links them through a signed route (GET /forms/files/{submission}/{field}/{index}); nothing has a guessable address. Deleting a submission deletes its files. The JSON API takes files as { "name": "brief.pdf", "data": "data:application/pdf;base64,…" } items.
Retention
Schedule form-builder:prune daily:
Schedule::command('form-builder:prune')->daily();
It deletes submissions (and their files) older than the form's Keep submissions for or submissions.retention_days (0 keeps everything), strips the IP address and user agent from those older than submissions.anonymize_after_days, and drops webhook deliveries older than webhooks.keep_days. --dry-run reports without deleting.
Notifications and webhooks
See Notifications and webhooks.
Events and sinks
Packstub\FormBuilder\Events\SubmissionReceived is dispatched with the form, the submission and the context; SpamDetected when a submission is dropped.
A sink receives every accepted submission:
use Packstub\FormBuilder\Contracts\SubmissionSink;
use Packstub\FormBuilder\Models\FormSubmission;
class PushToCrm implements SubmissionSink
{
public function handle(FormSubmission $submission): void
{
Crm::createLead($submission->form->slug, $submission->data);
}
}
Register sinks in the config (sinks) or at runtime (FormBuilder::sink(PushToCrm::class)). When the form does not store submissions, the sink receives an unsaved model with the form relation loaded. $submission->toPayload() gives the same array the webhook posts.
From code
use Packstub\FormBuilder\Facades\FormBuilder;
$result = FormBuilder::submit('contact', ['email' => 'ada@example.com', 'message' => 'Hi']);
$result->submission; // FormSubmission
Submissions from code skip the spam checks, the password and the captcha; validation, conditions and limits still apply and validation throws ValidationException.