Fluent Forms entries and data model
SkillAI & modelsReads, 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().
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Fluent Forms entries and data model skill
What this skill tells your AI
The instructions your AI receives, as published by lonsdale201/wp-agent-skills in fluentform/fluentform-entries-data/SKILL.md and read by ahel’s review.
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
SubmissionServiceso Fluent Forms hooks, files, logs, details, queued actions, and payment-related cleanup are considered. - If an existing response must be edited, treat
responseJSON and affectedentry_detailsrows 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 readfluentform_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. Usefluentform_manage_entriesfor 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
LIKEsearches over the largeresponseJSON 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 markunreadentries asreadby default in 6.2.7. - Do not use
FluentForm\App\Models\Entryas the primary model; the live model isFluentForm\App\Models\Submission, whileFluentForm\App\Api\Entryis 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-lifecyclefor creation-time data and hooks. - Use
fluentform-custom-fieldsfor field-name and nested-value contracts. - Use
wp-rest-apiwhen entries are exposed through a custom REST endpoint.
References
- Official database schema: https://developers.fluentforms.com/database/
- Official model guide: https://developers.fluentforms.com/database/models/
- Official query builder guide: https://developers.fluentforms.com/database/query-builder/
- Verified Free source paths:
fluentform/boot/globals.phpfluentform/app/Api/Form.phpfluentform/app/Api/Entry.phpfluentform/app/Api/Submission.phpfluentform/app/Models/Submission.phpfluentform/app/Models/EntryDetails.phpfluentform/app/Models/SubmissionMeta.phpfluentform/app/Services/Submission/SubmissionService.php
- Verified Pro source path:
fluentformpro/src/classes/StepFormEntries.php
Signals
- GitHub stars
- 22
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
fluentform-entries-data- Source
- github.com/lonsdale201/wp-agent-skills