Fluent Forms entries and data model
Use Fluent Forms' API/model layer after enforcing your own authorization. Keep
the canonical response snapshot, query projection, and addon metadata separate.
Read data-contract.md before writing entry data,
exposing it over REST, joining nested fields, or supporting Pro drafts/payments.
Availability contract
| Data/API |
Availability in 6.2.7 |
| forms, submissions, entry details, form/submission meta |
Free |
fluentFormApi(), FormFieldsParser, Submission, SubmissionMeta |
Free |
| draft/partial submissions |
Pro |
| order items, transactions, payment subscriptions and coupons |
Payment/Pro feature dependent |
The PHP helpers perform data access, not request authorization. A successful
fluentFormApi() call does not prove the current user may see the result.
Read one form's entries
use FluentForm\App\Modules\Acl\Acl;
$formId = absint($requestedFormId);
if (!$formId || !function_exists('fluentFormApi')) {
return new WP_Error('acme_unavailable', __('Fluent Forms is unavailable.', 'acme-addon'));
}
if (!Acl::hasPermission('fluentform_entries_viewer', $formId)) {
return new WP_Error('acme_forbidden', __('You cannot view these entries.', 'acme-addon'), [
'status' => 403,
]);
}
$form = fluentFormApi('forms')->find($formId);
if (!$form) {
return new WP_Error('acme_not_found', __('Form not found.', 'acme-addon'), [
'status' => 404,
]);
}
$page = max(1, absint($requestedPage));
$perPage = min(100, max(1, absint($requestedPerPage)));
$result = fluentFormApi('forms')->entryInstance($form)->entries([
'page' => $page,
'per_page' => $perPage,
'entry_type' => 'all',
'sort_type' => 'DESC',
'search' => sanitize_text_field((string) $requestedSearch),
]);
Use the form-scoped entryInstance() for a known form. The global
fluentFormApi('submissions') methods are useful for trusted internal reports,
but callers must constrain form IDs, user IDs, status, and page size themselves.
Read a single form-scoped entry
$entryResult = fluentFormApi('forms')
->entryInstance($form)
->entry(absint($entryId), false);
if (!$entryResult) {
return new WP_Error('acme_entry_not_found', __('Entry not found.', 'acme-addon'), [
'status' => 404,
]);
}
$entry = $entryResult['submission'];
$response = is_array($entry->response) ? $entry->response : [];
Do not fetch by entry ID globally and authorize with a different form ID. Scope
the database lookup and permission decision to the same normalized form ID.
Resolve field definitions and labels
use FluentForm\App\Modules\Form\FormFieldsParser;
$inputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'raw']);
$labels = FormFieldsParser::getAdminLabels($form, $inputs);
foreach ($response as $name => $value) {
$label = $labels[$name] ?? $name;
// Escape $label and $value for their actual output context.
}
attributes.name, not the visible label, connects the field definition to the
response. Labels and fields can change after old submissions were stored, so
always provide a fallback for historical/removed keys.
Store addon state as submission meta
use FluentForm\App\Helpers\Helper;
$entryId = absint($entryId);
$formId = absint($formId);
// First verify the entry belongs to $formId and the current operation is allowed.
Helper::setSubmissionMeta($entryId, '_acme_delivery_state', [
'status' => 'queued',
'updated_at' => current_time('mysql'),
], $formId);
$state = Helper::getSubmissionMeta($entryId, '_acme_delivery_state', []);
Namespace meta keys. Store bounded operational data, not credentials or copied
entry payloads. SubmissionMeta serializes values and is not encrypted.
Mutation policy
- Prefer submission-time filters when deriving a stored field value.
- For status changes and deletion, use
SubmissionService so Fluent Forms hooks,
files, logs, details, queued actions, and payment-related cleanup are considered.
- If an existing response must be edited, treat
response JSON and affected
entry_details rows as one consistency boundary. Validate against the current
form, preserve unknown historical keys deliberately, update the timestamp, and
emit the appropriate audit hook/log.
- Never update only
fluentform_entry_details; normal entry rendering and feeds
read fluentform_submissions.response.
- Never expose generic model
where/sort/column inputs directly to a request.
Security and performance rules
- Use
Acl::hasPermission('fluentform_entries_viewer', $formId) for Fluent Forms
admin semantics, plus any domain-specific ownership rule your endpoint needs.
Use fluentform_manage_entries for mutations.
- Add nonce verification to cookie-authenticated writes; a nonce does not replace
the capability/form-scope check.
- Return an explicit field allowlist. Entries can contain personal data, IP,
source URLs, hidden fields, payment fields, and addon-injected values.
- Bound
per_page, validate statuses, and use a fixed sort allowlist.
- Avoid
LIKE searches over the large response JSON column for unbounded public
queries. Use detail rows or an addon-owned indexed table for frequent reports.
- Do not use
SubmissionService::find() for a read-only probe without noticing
that it can mark unread entries as read by default in 6.2.7.
- Do not use
FluentForm\App\Models\Entry as the primary model; the live model is
FluentForm\App\Models\Submission, while FluentForm\App\Api\Entry is the
form-scoped API wrapper.
Pro boundary
Pro partial entries live in fluentform_draft_submissions and have a different
ownership/hash lifecycle. Do not merge them into completed-submission queries by
ID alone. Pro/payment records link through submission_id, but payment access
requires fluentform_view_payments or fluentform_manage_payments and must use
verified payment status, not merely the presence of a row.
Cross-references
- Use
fluentform-submission-lifecycle for creation-time data and hooks.
- Use
fluentform-custom-fields for field-name and nested-value contracts.
- Use
wp-rest-api when entries are exposed through a custom REST endpoint.
References
1---2name: fluentform-entries-data3description: Reads, relates, updates, and audits Fluent Forms forms, submissions, entry details, and submission meta from third-party plugins. Covers fluentFormApi, FormFieldsParser, Submission and SubmissionMeta models, form-scoped queries, response JSON versus normalized detail rows, pagination, capabilities, deletion hooks, and Free versus Pro tables. Use when building entry reports, exports, dashboards, REST endpoints, submission metadata, user-facing entry views, or code touching fluentform_submissions, fluentform_entry_details, fluentform_submission_meta, fluentFormApi('submissions'), or entryInstance().4---56# Fluent Forms entries and data model78Use Fluent Forms' API/model layer after enforcing your own authorization. Keep9the canonical response snapshot, query projection, and addon metadata separate.1011Read [data-contract.md](references/data-contract.md) before writing entry data,12exposing it over REST, joining nested fields, or supporting Pro drafts/payments.1314## Availability contract1516| Data/API | Availability in 6.2.7 |17|---|---|18| forms, submissions, entry details, form/submission meta | Free |19| `fluentFormApi()`, `FormFieldsParser`, `Submission`, `SubmissionMeta` | Free |20| draft/partial submissions | Pro |21| order items, transactions, payment subscriptions and coupons | Payment/Pro feature dependent |2223The PHP helpers perform data access, not request authorization. A successful24`fluentFormApi()` call does not prove the current user may see the result.2526## Read one form's entries2728```php29use FluentForm\App\Modules\Acl\Acl;3031$formId = absint($requestedFormId);3233if (!$formId || !function_exists('fluentFormApi')) {34 return new WP_Error('acme_unavailable', __('Fluent Forms is unavailable.', 'acme-addon'));35}3637if (!Acl::hasPermission('fluentform_entries_viewer', $formId)) {38 return new WP_Error('acme_forbidden', __('You cannot view these entries.', 'acme-addon'), [39 'status' => 403,40 ]);41}4243$form = fluentFormApi('forms')->find($formId);44if (!$form) {45 return new WP_Error('acme_not_found', __('Form not found.', 'acme-addon'), [46 'status' => 404,47 ]);48}4950$page = max(1, absint($requestedPage));51$perPage = min(100, max(1, absint($requestedPerPage)));5253$result = fluentFormApi('forms')->entryInstance($form)->entries([54 'page' => $page,55 'per_page' => $perPage,56 'entry_type' => 'all',57 'sort_type' => 'DESC',58 'search' => sanitize_text_field((string) $requestedSearch),59]);60```6162Use the form-scoped `entryInstance()` for a known form. The global63`fluentFormApi('submissions')` methods are useful for trusted internal reports,64but callers must constrain form IDs, user IDs, status, and page size themselves.6566## Read a single form-scoped entry6768```php69$entryResult = fluentFormApi('forms')70 ->entryInstance($form)71 ->entry(absint($entryId), false);7273if (!$entryResult) {74 return new WP_Error('acme_entry_not_found', __('Entry not found.', 'acme-addon'), [75 'status' => 404,76 ]);77}7879$entry = $entryResult['submission'];80$response = is_array($entry->response) ? $entry->response : [];81```8283Do not fetch by entry ID globally and authorize with a different form ID. Scope84the database lookup and permission decision to the same normalized form ID.8586## Resolve field definitions and labels8788```php89use FluentForm\App\Modules\Form\FormFieldsParser;9091$inputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'raw']);92$labels = FormFieldsParser::getAdminLabels($form, $inputs);9394foreach ($response as $name => $value) {95 $label = $labels[$name] ?? $name;96 // Escape $label and $value for their actual output context.97}98```99100`attributes.name`, not the visible label, connects the field definition to the101response. Labels and fields can change after old submissions were stored, so102always provide a fallback for historical/removed keys.103104## Store addon state as submission meta105106```php107use FluentForm\App\Helpers\Helper;108109$entryId = absint($entryId);110$formId = absint($formId);111112// First verify the entry belongs to $formId and the current operation is allowed.113Helper::setSubmissionMeta($entryId, '_acme_delivery_state', [114 'status' => 'queued',115 'updated_at' => current_time('mysql'),116], $formId);117118$state = Helper::getSubmissionMeta($entryId, '_acme_delivery_state', []);119```120121Namespace meta keys. Store bounded operational data, not credentials or copied122entry payloads. `SubmissionMeta` serializes values and is not encrypted.123124## Mutation policy125126- Prefer submission-time filters when deriving a stored field value.127- For status changes and deletion, use `SubmissionService` so Fluent Forms hooks,128 files, logs, details, queued actions, and payment-related cleanup are considered.129- If an existing response must be edited, treat `response` JSON and affected130 `entry_details` rows as one consistency boundary. Validate against the current131 form, preserve unknown historical keys deliberately, update the timestamp, and132 emit the appropriate audit hook/log.133- Never update only `fluentform_entry_details`; normal entry rendering and feeds134 read `fluentform_submissions.response`.135- Never expose generic model `where`/sort/column inputs directly to a request.136137## Security and performance rules138139- Use `Acl::hasPermission('fluentform_entries_viewer', $formId)` for Fluent Forms140 admin semantics, plus any domain-specific ownership rule your endpoint needs.141 Use `fluentform_manage_entries` for mutations.142- Add nonce verification to cookie-authenticated writes; a nonce does not replace143 the capability/form-scope check.144- Return an explicit field allowlist. Entries can contain personal data, IP,145 source URLs, hidden fields, payment fields, and addon-injected values.146- Bound `per_page`, validate statuses, and use a fixed sort allowlist.147- Avoid `LIKE` searches over the large `response` JSON column for unbounded public148 queries. Use detail rows or an addon-owned indexed table for frequent reports.149- Do not use `SubmissionService::find()` for a read-only probe without noticing150 that it can mark `unread` entries as `read` by default in 6.2.7.151- Do not use `FluentForm\App\Models\Entry` as the primary model; the live model is152 `FluentForm\App\Models\Submission`, while `FluentForm\App\Api\Entry` is the153 form-scoped API wrapper.154155## Pro boundary156157Pro partial entries live in `fluentform_draft_submissions` and have a different158ownership/hash lifecycle. Do not merge them into completed-submission queries by159ID alone. Pro/payment records link through `submission_id`, but payment access160requires `fluentform_view_payments` or `fluentform_manage_payments` and must use161verified payment status, not merely the presence of a row.162163## Cross-references164165- Use `fluentform-submission-lifecycle` for creation-time data and hooks.166- Use `fluentform-custom-fields` for field-name and nested-value contracts.167- Use `wp-rest-api` when entries are exposed through a custom REST endpoint.168169## References170171- Official database schema: <https://developers.fluentforms.com/database/>172- Official model guide: <https://developers.fluentforms.com/database/models/>173- Official query builder guide: <https://developers.fluentforms.com/database/query-builder/>174- Verified Free source paths:175 - `fluentform/boot/globals.php`176 - `fluentform/app/Api/Form.php`177 - `fluentform/app/Api/Entry.php`178 - `fluentform/app/Api/Submission.php`179 - `fluentform/app/Models/Submission.php`180 - `fluentform/app/Models/EntryDetails.php`181 - `fluentform/app/Models/SubmissionMeta.php`182 - `fluentform/app/Services/Submission/SubmissionService.php`183- Verified Pro source path:184 - `fluentformpro/src/classes/StepFormEntries.php`