diff --git a/README.md b/README.md index 433c28a..9dd5ece 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,18 @@ Skills follow the [Agent Skills](https://agentskills.io/) format. ## Installation -### CLI (coming soon) +### Skills CLI -> The `@gravity/skills` CLI is not yet published. Once available: +> Until the `@gravity/skills` CLI is published, use the [Vercel Skills CLI](https://github.com/vercel-labs/skills): ```bash -npx @gravity/skills install +npx skills add gravityforms/gravityskills +``` + +Install a specific skill: + +```bash +npx skills add gravityforms/gravityskills --skill gravity-forms-abilities ``` ### GitHub Releases @@ -20,7 +26,15 @@ Download skill zips directly from [Releases](https://github.com/gravityforms/gra ## Available Skills -_Coming soon._ This repository is being set up to host public skills for Gravity products. +### [`gravity-forms-abilities`](skills/gravity-forms-abilities/) + +Workflow guidance for AI agents using Gravity Forms abilities via the WordPress Abilities API (MCP). Covers form CRUD, entry management, submissions, feeds, notifications, conditional logic, and system queries. Provides critical sequencing rules, field configuration knowledge, and pitfall avoidance that tool schemas alone cannot express. + +**Requires:** WordPress site with Gravity Forms 2.9+ and MCP endpoint enabled (GF Settings → MCP). + +```bash +npx skills add gravityforms/gravityskills --skill gravity-forms-abilities +``` ## Usage diff --git a/skills/gravity-forms-abilities/SKILL.md b/skills/gravity-forms-abilities/SKILL.md new file mode 100644 index 0000000..ea10296 --- /dev/null +++ b/skills/gravity-forms-abilities/SKILL.md @@ -0,0 +1,356 @@ +--- +name: gravity-forms-abilities +description: Workflow guidance for AI agents using Gravity Forms abilities via the WordPress Abilities API (MCP). Load this skill when interacting with any gravityforms/* MCP tools — creating/updating forms, managing entries, submitting data, searching entries, managing feeds/notifications, analyzing conditional logic, or querying system info. Provides critical sequencing rules, field configuration knowledge, conditional logic patterns, and pitfall avoidance that tool schemas alone cannot express. +license: GPL-2.0+ +compatibility: Requires a WordPress site with Gravity Forms 2.9+ and the MCP endpoint enabled (GF Settings → MCP) +metadata: + author: gravityforms + version: "1.0.0" +--- + +# Gravity Forms Abilities — Agent Skill + +## Ability Routing + +25 abilities across 7 categories. Select based on intent: + +**Discover** → `system-field-types` (field types), `system-info` (site identity, version, license, add-ons — includes `site_url` and `site_name` for multi-site disambiguation), `forms-list` (all forms as summaries: id, title, is_active, date_created, field_count), `forms-get` (single form with full field detail) + +**Build** → `forms-create` (new form), `forms-update` (modify form), `forms-duplicate` (clone form), `forms-delete` (trash by default, `force: true` for permanent) + +**Analyze** → `forms-analyze-logic` (conditional logic audit: field/notification/confirmation/button logic with dependency map) + +**Collect data** → `submissions-submit` (full pipeline: validate → entry → notifications → feeds), `submissions-validate` (dry-run validation only) + +**Manage data** → `entries-search` (filter/paginate), `entries-get` (single entry), `entries-count` (efficient count), `entries-create` (raw insert, no validation), `entries-update`, `entries-delete` (trash by default; single by entry_id OR bulk by form_id + optional search_criteria; `force: true` for permanent) + +**Integrations** → `feeds-list`, `feeds-create`, `feeds-update`, `feeds-delete` + +**Notifications** → `notifications-list`, `notifications-send` (re-send for existing entry) + +**Audit** → `notes-list`, `notes-add` (annotate entries) + +All ability names use format `gravityforms/{category}-{action}`. + +## Access Modes + +The MCP settings page (GF Settings → MCP) controls two toggles: + +1. **Enable MCP** (default: off) — Gates all ability registration. When off, no `gravityforms/*` abilities appear. +2. **Write Access** (default: off) — When off, only read-only abilities are registered (12 of 25). Write abilities (create, update, delete, submit, send, add) are filtered out at registration time. + +**Read-only abilities** (always available when MCP is enabled): +`forms-get`, `forms-list`, `forms-analyze-logic`, `entries-get`, `entries-search`, `entries-count`, `submissions-validate`, `feeds-list`, `notifications-list`, `notes-list`, `system-info`, `system-field-types` + +**Write abilities** (require Write Access toggle): +`forms-create`, `forms-update`, `forms-delete`, `forms-duplicate`, `entries-create`, `entries-update`, `entries-delete`, `feeds-create`, `feeds-update`, `feeds-delete`, `submissions-submit`, `notifications-send`, `notes-add` + +If a write ability is not available, the site admin has not enabled write access. Do not attempt workarounds — inform the user that write access must be enabled in GF Settings → MCP. + +### Endpoint Modes + +The MCP settings page also controls how GF abilities are exposed: + +- **Site MCP** (default) — GF abilities are registered on the shared WordPress MCP endpoint (`/wp-json/mcp/v1`). They appear alongside abilities from other plugins and are accessed through the default server's meta-tools (`discover-abilities`, `execute-ability`, `get-ability-schema`). +- **Dedicated Endpoint** — GF registers its own MCP server at `/wp-json/mcp/gravityforms`. Each ability becomes a direct MCP tool (e.g., `gravityforms-forms-get` instead of going through `execute-ability`). GF abilities are hidden from the default server. + +**When using dedicated endpoint mode:** +- Tool names use hyphen format: `gravityforms-forms-get`, `gravityforms-entries-search`, etc. (the `/` in ability names is converted to `-`) +- Each tool has its own full JSON Schema — no need to call `get-ability-schema` first +- The MCP client must be configured to connect to the GF server endpoint separately +- Both servers can coexist — the default server serves other plugins' abilities while GF serves its own + +**When using site MCP mode (default):** +- All abilities accessed through the shared endpoint's `execute-ability` meta-tool +- Tool name passed as a parameter: `execute-ability` with `{"ability": "gravityforms/forms-get", ...}` +- This is the simpler setup — one MCP connection covers all plugins + +The agent does not need to know which mode is active — the MCP client handles routing. The same abilities are available in both modes; only the transport differs. + +## Critical Workflows + +### Creating a Form + +1. Call `system-field-types` — discover available types and capabilities +2. Build form object with `title` and `fields` array +3. **Include a `notifications` object** — `forms-create` does NOT auto-create a default admin notification (unlike the GF admin UI). Without one, submissions are saved but no email is sent. +4. Call `forms-create` — returns `form_id` and `edit_url` + +Never guess field types. `system-field-types` returns `supports_choices`, `has_inputs`, `default_inputs`, and support flags for each type. + +**Default admin notification template:** +```json +{ + "notifications": { + "notif_admin": { + "id": "notif_admin", + "name": "Admin Notification", + "event": "form_submission", + "toType": "email", + "to": "{admin_email}", + "subject": "New submission from {form_title}", + "message": "{all_fields}", + "isActive": true + } + } +} +``` + +Use `{admin_email}` for site admin, or a specific address. `{all_fields}` renders all submitted values. `{form_title}` inserts the form name. + +### Updating a Form + +**The `fields` array replaces ALL existing fields.** Sending partial arrays deletes omitted fields. + +1. Call `forms-get` — retrieve current form structure +2. Modify the returned fields array (add/remove/change) +3. Call `forms-update` with the complete form object + +`notifications` and `confirmations` merge by key — safe for partial updates. `fields` does NOT merge. + +### Submitting a Form + +1. Call `forms-get` — learn field IDs and structure +2. Build `input_values` using `input_{field_id}` key format +3. Call `submissions-submit` +4. Check `is_valid` in response — if false, read `validation_messages` + +For most compound fields (name, address), use dot-notation sub-input keys: `input_{field_id}.{suffix}` (e.g., `input_5.3` for First Name on field 5). This is the canonical format matching how GF stores sub-input IDs internally. + +**Time fields are the exception:** submit via underscore-style sibling keys that the bridge normalizes: `input_6_1`, `input_6_2`, `input_6_3` for Hour / Minute / AM-PM. This underscore format is specific to time fields only — all other compound fields use dot notation. Do not conclude a time field is broken until you've tested those three sub-inputs together on a clean form. + +For **multiselect fields**, pass values as an array: `"input_3": ["Red", "Blue"]`. Never use comma-separated strings — values containing commas cause data loss. + +**`submissions-submit` vs `entries-create`:** +- `submissions-submit` = full pipeline (validation → entry → notifications → feeds) +- `entries-create` = raw DB insert, bypasses everything — use only for data migration + +### Pausing / Scheduling a Form + +**Do NOT use `is_active: '0'` to pause a form** — that deactivates it entirely (shortcode renders nothing). Use scheduling instead. + +**Pause immediately** (form page stays up, shows message): +1. Call `forms-list` with `search` — find the form +2. Call `forms-update`: + - `scheduleForm: true` + - `scheduleEnd` set to yesterday's date (MM/DD/YYYY format) + - `scheduleMessage: "This form is no longer accepting submissions."` + +**Schedule a form window** (open during a date range): +1. Call `forms-update`: + - `scheduleForm: true` + - `scheduleStart: "01/15/2026"`, `scheduleEnd: "02/15/2026"` + - `schedulePendingMessage: "Registration opens January 15th."` + - `scheduleMessage: "Registration has closed."` + +**Block a date range** (e.g., "close reservations Dec 20–Jan 2"): + +GF scheduling is open-window only — there is no "block these dates" mode. To block a date range: +1. Set `scheduleEnd` to the day before the closure starts (e.g., `12/19/2026`) +2. Set `scheduleStart` to today (or leave existing start) +3. Set `scheduleMessage` explaining the closure and when the form reopens +4. **After the closure ends**, call `forms-update` with `scheduleForm: false` to reopen + +This is a two-touch workflow — the agent cannot set-and-forget a future block window. + +**Reopen a paused form:** +1. Call `forms-update` with `scheduleForm: false` + +**Cap entries** (stop after N submissions): +1. Call `forms-update`: + - `limitEntries: true` + - `limitEntriesCount: 500` + - `limitEntriesPeriod: ""` (total) or `"day"`, `"week"`, `"month"`, `"year"` + - `limitEntriesMessage: "Registration is full."` + +### Duplicating and Customizing a Form + +1. Call `forms-list` with `search` — find the source form by title +2. Call `forms-duplicate` — creates an exact copy (title gets " (1)" suffix) +3. Call `forms-get` on the **new** form — inspect everything that needs updating +4. Call `forms-update` — rename title, update fields, confirmations, and notifications + +**Don't just update the title and fields.** After duplicating, review and update ALL content that references the original: +- **Field labels, descriptions, placeholders, default values** — dates, event names, seasonal references +- **Choice text/values** — date options in dropdowns or radio buttons +- **Confirmation messages** — "Thanks for registering for Q2..." → update to Q4 +- **Notification subjects and bodies** — email content referencing original form's context +- **Form description** — displayed to users above the form + +Remember: `fields` array replaces ALL fields on update. Always pass the complete array from `forms-get` with your modifications. + +### Multi-Form Batch Update (e.g., Add Field to All Forms) + +For scenarios like "add a GDPR consent checkbox to every form that collects personal information": + +1. Call `forms-list` — get all forms +2. **For each form**: call `forms-get` → inspect fields to determine if it qualifies (e.g., has name/email/phone/address fields) and doesn't already have the target field (e.g., a consent field) +3. For qualifying forms: append the new field to the existing fields array → call `forms-update` + +**This is chatty but functional.** A site with 100 forms requires 100 `forms-get` calls for inspection plus update calls for qualifying forms. There is no bulk "get all forms with full fields" ability. Work form-by-form. + +**Tips:** +- Always check for the field type before adding — avoid duplicating consent fields on forms that already have one +- Use `nextFieldId` from `forms-get` as the new field's `id`, and bump `nextFieldId` by 1 in the update +- Remember: `fields` array on `forms-update` replaces ALL fields — always pass the complete array from `forms-get` with your addition appended + +### Searching Entries + +1. Call `forms-get` to learn field IDs +2. Call `entries-search` with `field_filters` using field IDs (numbers), not labels +3. For large sets, call `entries-count` first, then paginate with `paging.page_size` (max 100) + +**When a user asks for "an entry" without a date:** run a broad `entries-search` first (no date filter). Only narrow to today / this week / a specific date after you confirm there are matching entries in that scope. A zero-result date filter does **not** prove the form has no entries. + +**Batch counting:** To compare entry counts across multiple forms (e.g., "which forms have no submissions this month?"), pass an array of form IDs to `entries-count`: `{"form_ids": [1, 3, 6], "search_criteria": {...}}` → returns `{"total": 17, "counts": {"1": 5, "3": 0, "6": 12}}`. One call instead of N. + +**Time-bucketed reporting** (e.g., "monthly breakdown of enquiries this term"): + +There is no server-side group-by. The agent makes one `entries-count` call per time bucket: + +1. `forms-list` with `search` → find the form +2. `entries-count` per bucket with `start_date`/`end_date` — e.g., `{"start_date": "2026-01-01", "end_date": "2026-01-31"}` for January +3. Synthesize results into a table/summary for the user + +For a 3-month term that's 4 calls (1 form lookup + 3 counts). For a full year, 13 calls. Both formats work for dates: `"2026-01-01"` or `"2026-01-01 00:00:00"`. + +See [references/entry-operations.md](references/entry-operations.md) for filter operators and search examples. + +### Bulk Deleting Entries + +`entries-delete` supports bulk mode — pass `form_id` instead of `entry_id` to delete all matching entries server-side in one call. + +**Delete all entries from every form** (e.g., pre-launch cleanup): +1. Call `forms-list` — get all form IDs +2. Call `entries-count` across **all relevant form IDs** — get counts +3. **Confirm with the user** — "Form X has N entries, Form Y has M entries. Delete all?" +4. On confirmation, call `entries-delete` with `form_id` for each form (default: moves to trash; add `force: true` for permanent deletion) + +Never stop after deleting a single entry unless the user explicitly scoped the request to one entry. Sitewide cleanup requires enumerating the full form set first. + +**Delete filtered entries** (e.g., entries before a date): +1. Call `entries-delete` with `form_id` + `search_criteria` (same format as `entries-search`) + +See [references/entry-operations.md](references/entry-operations.md) for bulk delete examples. + +### Destructive Operations + +**Both `forms-delete` and `entries-delete` use soft delete (trash) by default.** The `force: true` parameter is required for permanent deletion. + +- Default behavior (`force` omitted or `false`): moves to trash. Forms get `is_trash = 1`, entries get `status = 'trash'`. Both are recoverable from the GF admin. +- `force: true`: permanently deletes. Cannot be undone. All associated data (entries, notes, meta, feeds for forms) is also deleted. +- The response includes `trashed: true/false` to confirm which path was taken. + +**Always preview and confirm before destructive actions.** This applies to: +- `entries-delete` (bulk mode) — call `entries-count` first, show scope to user +- `forms-delete` — confirm form title and entry count with user +- Any permanent deletion (`force: true`) + +Pattern: **count → report → confirm → execute**. + +### Multi-Site Management + +When an agent is connected to multiple WordPress sites via separate MCP servers, each site exposes identical GF abilities under a different tool prefix (e.g., `acme-site_mcp-adapter-...`, `bakery_mcp-adapter-...`). + +**Site identification:** Call `system-info` on each connected site first. The response includes `site_url` and `site_name`, which identify the physical site regardless of the MCP server name in the client config. + +**Cross-site queries** (e.g., "summary of all forms across all three sites"): +1. Call `system-info` on each site — get site name/URL and totals +2. Call `forms-list` on each site — get form summaries +3. Merge and present results, clearly labeling each form with its site + +**Cross-site operations** (e.g., "add GDPR consent to all forms on all sites"): +1. Call `system-info` on each site — confirm site identity +2. Work site-by-site using the Multi-Form Batch Update workflow +3. Report results per-site + +**Key rules:** +- **Form IDs are site-local.** Form ID 1 on site A is unrelated to form ID 1 on site B. Always track `(site, form_id)` pairs. +- **No cross-site abilities exist.** The agent is the aggregation layer — each ability call targets exactly one site. +- **Confirm site identity before destructive operations.** Call `system-info` to verify `site_url` matches the intended target. + +**Cross-site form cloning** (e.g., "copy the contact form from Client A to Client B"): +1. Call `forms-get` on the source site — retrieve full form structure +2. Clean the response for `forms-create`: + - **Strip**: form-level `id`, `date_created`, `is_active`, `is_trash`, `markupVersion` + - **Strip from each field**: `formId` (GFAPI auto-sets this to the new form's ID) + - **Strip computed properties**: `checked_indicator_url`, `checked_indicator_markup` (regenerated by GF) + - **Keep**: field `id` values — preserves merge tag references like `{Name:1}` in notifications and confirmations + - **Keep**: `notifications`, `confirmations`, `cssClass`, `size`, `visibility`, `pageNumber`, all type-specific config +3. Optionally update: `title`, notification `to` addresses, confirmation messages +4. Call `forms-create` on the target site with the cleaned form object +5. Verify with `forms-get` on the new form + +**Important:** Field `id` values are preserved through `forms-create` — GFAPI honors the IDs you pass. This means merge tags in notifications (e.g., `{Email:3}`) continue to reference the correct fields. `nextFieldId` is auto-calculated. + +## Conditional Logic + +Conditional logic (CL) controls show/hide behavior of fields, notifications, confirmations, submit button, and page buttons based on field values. + +### Analyzing Logic + +Call `forms-analyze-logic` instead of parsing raw CL from `forms-get`. It returns: +- **`summary`** — rule counts across all locations +- **`dependency_map`** — reverse map showing what each source field controls (most useful view) +- Per-location arrays: `field_logic`, `notification_logic`, `confirmation_logic`, `submit_button_logic`, `page_logic` + +### Adding/Modifying Logic + +CL is a property on individual fields (or notifications/confirmations). To add or change: + +1. `forms-get` → retrieve current form +2. Set `conditionalLogic` on the target field: `{ "actionType": "show", "logicType": "all", "rules": [{ "fieldId": "1", "operator": "is", "value": "Yes" }] }` +3. `forms-update` with complete fields array +4. `forms-analyze-logic` to verify + +**`fieldId` in rules must be a string** — `"1"` not `1`. + +For CL structure details, operators, and common patterns, see [references/conditional-logic.md](references/conditional-logic.md). + +## Key Pitfalls + +| Mistake | Consequence | Prevention | +|---|---|---| +| Skip `system-field-types` before `forms-create` | Invalid field types, missing choices config | Always call first | +| Partial `fields` array on `forms-update` | Deletes all omitted fields | Always `forms-get` → modify full array → `forms-update` | +| Missing `choices` on select/radio/checkbox | Field renders empty | Check `supports_choices` from `system-field-types` | +| Wrong input key format: `"1"` vs `"input_1"` | Silent data loss | `submissions-submit` uses `input_{id}`, `entries-create` uses `"{id}"` | +| Compound field without sub-input suffixes | Data not captured | Use `default_inputs` from `system-field-types`; for time fields send Hour/Minute/AM-PM together | +| Not checking `is_valid` after submission | Miss validation failures | Always check response `is_valid` field | +| Using `@example.com` emails in submissions | Rejected as spam by GF email field | Use realistic test domains (e.g., `@testmail.dev`) | +| Passing `form_id` as top-level param on `forms-update` | Input validation error | Put form ID inside the `form` object as `id` | +| Using integer `fieldId` in CL rules | Logic may not evaluate correctly | Always use string: `"fieldId": "1"` not `"fieldId": 1` | +| Parsing raw CL from `forms-get` manually | Error-prone, misses notifications/confirmations/buttons | Use `forms-analyze-logic` instead | +| Using `is_active: '0'` to pause a form | Form disappears entirely — page shows nothing | Use `scheduleForm` + `scheduleEnd` instead | +| Fileupload field without `allowedExtensions`/`maxFileSize` | Accepts any file type/size — security risk | Always set `allowedExtensions` and `maxFileSize` — see field-config reference | +| Creating a form without `notifications` | Submissions saved but no email sent — admin never notified | Always include a notification object — see "Creating a Form" workflow | +| Creating a name field without `nameFormat: "advanced"` | First/Last sub-inputs render stacked vertically instead of side-by-side | Always set `"nameFormat": "advanced"` and `"size": "large"` on name fields — see field-config reference | +| Passing multiselect values as comma-separated string | Data loss when values contain commas (e.g., "Atlanta, GA") | Always pass multiselect values as an **array**: `"input_1": ["Red", "Blue"]` — see field-config reference | +| User asks to "create a form and add it to a page" | Cannot create WordPress pages/posts — only GF abilities exist | Create the form, then tell the user the shortcode `[gravityform id="X" title="true"]` to embed manually. Page/post creation is not currently available via the Abilities API. | +| Write ability not found (e.g., `forms-create`) | Site admin has not enabled Write Access in MCP settings | Inform the user that write access must be enabled in GF Settings → MCP before write operations are available. Do not attempt workarounds. | +| `system-field-types` does not list rating/survey-style fields | The required add-on is not active on this site | Call `system-info` to check active add-ons; fall back to core fields (`radio`, `select`, `checkbox`) when add-on types are unavailable | +| `entries-update` with only status/metadata | Older bridge versions could wipe omitted field values | Safest pattern is still fetch → merge → update when changing existing entries | +| `entries-create` date field given in the wrong format | Searches may miss the entry later | Prefer the form's configured date format; ISO is only safe if the bridge explicitly normalizes it | +| "First/last name side by side" using a compound `name` field + layout grid | The `name` field already renders its sub-inputs (Prefix/First/Last/Suffix) in a row internally — adding `layoutGridColumnSpan` controls the whole block's width, not individual sub-inputs | When the user wants "first name and last name side by side," use **two separate `text` fields** with shared `layoutGroupId` and span `6`, not a single compound `name` field. Use the compound `name` field only when you want the full name widget (prefix/first/last/suffix). | + +## Field Configuration Quick Reference + +**Choice fields** (`supports_choices: true`): `select`, `radio`, `checkbox`, `multiselect`, `image_choice` — require `choices: [{text, value}]` array. **Multiselect values must be submitted as arrays** (`"input_1": ["Red", "Blue"]`), not comma-separated strings. + +**Compound fields** (`has_inputs: true`): `name`, `address`, `time` — store values across sub-inputs with ID suffixes. Call `system-field-types` to see `default_inputs` for exact suffix mappings. **Name fields require `"nameFormat": "advanced"` and `"size": "large"`** — without these, sub-inputs render stacked instead of side-by-side. + +**File upload fields**: `fileupload` — configure `allowedExtensions` (comma-separated, no dots), `maxFileSize` (MB), `maxFiles`, `multipleFiles`. These properties are NOT returned by `system-field-types` — see [references/field-config.md](references/field-config.md) for full type-specific config reference. File upload fields can be configured via abilities, but actual file submission requires the rendered form (MCP cannot transport binary data). + +**Consent fields**: `consent` — GDPR-style checkbox with `checkboxLabel` (the agreement text next to the checkbox) and `description` (longer explanatory text below). Always set `isRequired: true` for GDPR compliance. See [references/field-config.md](references/field-config.md) for examples. + +**Layout fields** (no data): `html`, `section`, `page` — visual only, do not collect submissions. + +**Layout grid**: Control field width and row grouping with `layoutGridColumnSpan` (1–12, default full width) and `layoutGroupId` (any string — fields sharing the same value render on the same row). See [references/field-config.md](references/field-config.md) §Layout Grid for patterns and natural language mapping. + +**Type-specific properties**: Several field types accept configuration beyond what `system-field-types` reports — `number` (rangeMin/rangeMax/numberFormat), `date` (dateFormat/dateType), `phone` (phoneFormat), `text`/`textarea` (maxLength), `fileupload` (allowedExtensions/maxFileSize/maxFiles), `consent` (checkboxLabel/description). See [references/field-config.md](references/field-config.md) for the full reference. + +For detailed field type tables, compound field suffix mappings, and form design patterns, see [references/field-config.md](references/field-config.md). + +For entry search filters, input value formatting, and submission examples, see [references/entry-operations.md](references/entry-operations.md). + +For conditional logic structure, operators, dependency maps, and CL modification patterns, see [references/conditional-logic.md](references/conditional-logic.md). diff --git a/skills/gravity-forms-abilities/references/conditional-logic.md b/skills/gravity-forms-abilities/references/conditional-logic.md new file mode 100644 index 0000000..1229c1d --- /dev/null +++ b/skills/gravity-forms-abilities/references/conditional-logic.md @@ -0,0 +1,204 @@ +# Conditional Logic Reference + +## Overview + +Conditional logic (CL) controls visibility and behavior of form elements based on field values. CL can be applied to fields, notifications, confirmations, the submit button, and page navigation buttons. + +Use `forms-analyze-logic` to get a structured analysis rather than parsing raw CL from `forms-get`. + +## CL Structure + +Every CL block follows this structure: + +```json +{ + "enabled": true, + "actionType": "show", + "logicType": "all", + "rules": [ + { + "fieldId": "1", + "operator": "is", + "value": "Yes" + } + ] +} +``` + +| Property | Values | Meaning | +|---|---|---| +| `enabled` | `true` / `false` | Whether this CL is active | +| `actionType` | `"show"` / `"hide"` | What happens when rules match | +| `logicType` | `"all"` / `"any"` | ALL rules must match vs ANY rule | +| `rules[]` | array | Conditions that drive the action | + +## Rule Operators + +| Operator | Meaning | Works With | +|---|---|---| +| `is` | Exact match | All field types | +| `isnot` | Not equal | All field types | +| `contains` | Substring match | Text, textarea, email | +| `>` | Greater than | Number, date | +| `<` | Less than | Number, date | +| `>=` | Greater or equal | Number, date | +| `<=` | Less or equal | Number, date | + +## Where CL Appears + +| Location | Path in Form Data | Effect | +|---|---|---| +| **Fields** | `field.conditionalLogic` | Show/hide the field | +| **Notifications** | `notification.conditionalLogic` | Send/don't send the notification | +| **Confirmations** | `confirmation.conditionalLogic` | Select which confirmation to display | +| **Submit button** | `form.button.conditionalLogic` | Show/hide the submit button | +| **Page buttons** | `field.nextButton.conditionalLogic` (page fields only) | Show/hide the Next button on multi-page forms | + +## forms-analyze-logic Output + +The ability returns: + +```json +{ + "form_id": 41, + "form_title": "My Form", + "summary": { + "total_rules": 5, + "fields_with_logic": 2, + "notifications_with_logic": 1, + "confirmations_with_logic": 0, + "has_submit_button_logic": false, + "has_page_button_logic": false + }, + "field_logic": [ + { + "field_id": 3, + "field_label": "Comments", + "field_type": "textarea", + "action": "show", + "logic_type": "all", + "rules": [ + { + "source_field_id": "1", + "source_field_label": "Want to leave feedback?", + "operator": "is", + "value": "Yes" + } + ] + } + ], + "notification_logic": [], + "confirmation_logic": [], + "submit_button_logic": null, + "page_logic": [], + "dependency_map": { + "1": { + "label": "Want to leave feedback?", + "controls": [ + { + "target_type": "field", + "target_id": 3, + "target_label": "Comments", + "effect": "show", + "logic_type": "all" + } + ] + } + } +} +``` + +### Key Sections + +- **`summary`** — Quick counts. Check this first to decide if deeper analysis is needed. +- **`field_logic`** — Each field with CL, its action, and rules with resolved labels. +- **`notification_logic`** / **`confirmation_logic`** — Same structure for notifications and confirmations. +- **`submit_button_logic`** — `null` if no CL on submit button, otherwise `{action, logic_type, rules}`. +- **`page_logic`** — Array of page button CL (multi-page forms only). +- **`dependency_map`** — **Most useful view.** Keyed by source field ID, shows everything that field controls. Use this to answer "what happens when field X changes?" + +## Workflow: Auditing Form Logic + +1. Call `forms-analyze-logic` with the form ID +2. Check `summary` for a quick overview +3. Read `dependency_map` to understand which fields are "driver" fields +4. Review specific `field_logic` / `notification_logic` entries for rule details + +## Workflow: Adding CL to a Field via forms-update + +CL is set as a property on individual fields during `forms-update`: + +1. Call `forms-get` to retrieve current form +2. Add `conditionalLogic` to the target field in the fields array: + ```json + { + "id": 3, + "type": "textarea", + "label": "Comments", + "conditionalLogic": { + "actionType": "show", + "logicType": "all", + "rules": [ + { "fieldId": "1", "operator": "is", "value": "Yes" } + ] + } + } + ``` +3. Call `forms-update` with the complete fields array +4. Call `forms-analyze-logic` to verify the logic was applied correctly + +**Important:** `fieldId` in rules is a string, not an integer. Always quote it. + +## Common Patterns + +### Show field when another field has a specific value + +```json +{ + "actionType": "show", + "logicType": "all", + "rules": [{ "fieldId": "2", "operator": "is", "value": "Other" }] +} +``` + +### Hide field when multiple conditions are met + +```json +{ + "actionType": "hide", + "logicType": "all", + "rules": [ + { "fieldId": "1", "operator": "is", "value": "No" }, + { "fieldId": "5", "operator": "is", "value": "" } + ] +} +``` + +### Send notification only for specific selections + +Set on the notification object (via `forms-update` on the `notifications` property): + +```json +{ + "conditionalLogic": { + "actionType": "show", + "logicType": "any", + "rules": [ + { "fieldId": "4", "operator": "is", "value": "urgent" }, + { "fieldId": "4", "operator": "is", "value": "critical" } + ] + } +} +``` + +Note: For notifications, `actionType: "show"` means "send this notification when rules match." + +## Pitfalls + +| Mistake | Consequence | Prevention | +|---|---|---| +| Using integer `fieldId` in rules | CL may not evaluate correctly | Always use string: `"fieldId": "1"` not `"fieldId": 1` | +| Setting CL on a field that references itself | Circular dependency | CL rules should reference OTHER fields | +| Forgetting `forms-get` before `forms-update` | `fields` replaces all — CL on other fields lost | Always retrieve → modify → update | +| Not checking `forms-analyze-logic` after changes | Logic may not work as expected | Verify after every CL modification | +| Confusing notification `"show"` with field `"show"` | Notification `"show"` = "send when matched" | Read context: field=visibility, notification=delivery | diff --git a/skills/gravity-forms-abilities/references/entry-operations.md b/skills/gravity-forms-abilities/references/entry-operations.md new file mode 100644 index 0000000..2ea81c3 --- /dev/null +++ b/skills/gravity-forms-abilities/references/entry-operations.md @@ -0,0 +1,216 @@ +# Entry Operations Reference + +## Input Value Formatting + +### For `submissions-submit` (like a browser POST) + +Key format: `input_{field_id}` for simple fields, `input_{field_id}.{suffix}` for compound fields. + +```json +{ + "form_id": 1, + "input_values": { + "input_1": "Hello", + "input_5.3": "John", + "input_5.6": "Doe", + "input_8": "john@example.com" + } +} +``` + +### For `entries-create` (raw database insert) + +Key format: `"{field_id}"` for simple fields, `"{field_id}.{suffix}"` for compound fields. + +```json +{ + "entry": { + "form_id": 1, + "1": "Hello", + "5.3": "John", + "5.6": "Doe", + "8": "john@example.com" + } +} +``` + +**These are different formats.** `submissions-submit` prefixes with `input_`. `entries-create` uses bare field IDs. + +### Checkbox Values + +Each checked choice is a separate input keyed by choice index (starting at 1): + +```json +{ + "input_4.1": "choice_value_1", + "input_4.3": "choice_value_3" +} +``` + +Only include checked values — omit unchecked choices entirely. + +## Entry Search + +### Filter Operators + +`is`, `isnot`, `contains`, `>`, `<`, `>=`, `<=` + +The `contains` operator does literal substring matching (SQL LIKE). It is **not** stemmed — "involved" will not match "involve." + +### Special Filter Keys (Entry Meta) + +`date_created`, `date_updated`, `created_by`, `ip`, `source_url`, `status`, `is_starred`, `is_read` + +### All-Field Text Search + +Use `key: "0"` to search across ALL field values in a single filter: + +```json +{ "key": "0", "value": "involved", "operator": "contains" } +``` + +This searches every field on every entry — no need to know specific field IDs. Use this when the user asks to "find entries that mention X" without specifying which field. + +**Note:** Multiple `field_filters` default to AND logic. Use `field_filters_mode: "any"` in search_criteria for OR logic: + +```json +{ + "search_criteria": { + "field_filters": [ + { "key": "4", "value": "volunteer", "operator": "contains" }, + { "key": "5", "value": "involved", "operator": "contains" } + ], + "field_filters_mode": "any" + } +} +``` + +`"all"` (default) = every filter must match. `"any"` = at least one filter must match. + +### Search Example + +```json +{ + "form_ids": 1, + "search_criteria": { + "status": "active", + "field_filters": [ + { "key": "1", "value": "John", "operator": "contains" }, + { "key": "created_by", "value": "1", "operator": "is" } + ], + "start_date": "2025-01-01", + "end_date": "2025-12-31" + }, + "sorting": { "key": "date_created", "direction": "DESC" }, + "paging": { "offset": 0, "page_size": 20 } +} +``` + +### Date Filtering + +`start_date` and `end_date` accept two formats: +- Date only: `"2025-01-01"` +- Full datetime: `"2025-01-01 00:00:00"` + +Both work identically. Date-only is simpler when you don't need time precision. + +### Pagination Pattern + +For large result sets: + +1. Call `entries-count` to get total +2. Calculate pages: `total / page_size` +3. Iterate with `paging.offset` increments of `page_size` +4. Max `page_size` is 100 + +## Bulk Entry Deletion + +`entries-delete` supports bulk mode — pass `form_id` instead of `entry_id` to delete all entries for a form in a single call. + +### Delete all entries for one form + +```json +{ + "form_id": 5 +} +``` + +Returns: `{ "success": true, "deleted_count": 47 }` + +### Delete with filter (e.g., only entries before a date) + +```json +{ + "form_id": 5, + "search_criteria": { + "end_date": "2026-03-31 23:59:59" + } +} +``` + +### Delete all entries across all forms + +1. Call `forms-list` → get all form IDs +2. For each form, call `entries-delete` with `form_id` + +This is N calls (one per form), not N×M (one per entry). + +### ⚠️ Destructive Operations — Confirm First + +**Always preview before bulk deleting.** Call `entries-count` with the same `form_id` and `search_criteria` first to show the user how many entries will be deleted. Wait for explicit confirmation before calling `entries-delete`. + +``` +Agent: "Form 5 has 47 entries matching your criteria. Delete all 47? This cannot be undone." +User: "Yes, go ahead." +Agent: [calls entries-delete] +``` + +## Notification Resend + +### Find Entry → Resend Notification Workflow + +When a user asks to resend a notification for a specific entry (e.g., "resend the confirmation email to the parent who submitted last Tuesday"): + +1. **Find the form**: `forms-list` with `search` to identify the form by name +2. **Find the entry**: `entries-search` with `start_date`/`end_date` to narrow by date, plus `field_filters` if you have identifying info (name, email) +3. **Discover notifications**: `notifications-list` with `form_id` to see available notifications and their IDs +4. **Resend**: `notifications-send` with `form_id`, `entry_id`, and optionally `notification_ids` to target specific notifications + +### Example: Resend confirmation from last Tuesday + +```json +// Step 1: Find form +{ "ability": "gravityforms/forms-list", "params": { "search": "uniform order" } } + +// Step 2: Search entries by date range (Tuesday to Tuesday) +{ "ability": "gravityforms/entries-search", "params": { + "form_ids": 307, + "search_criteria": { "start_date": "2026-03-31", "end_date": "2026-03-31" }, + "sorting": { "key": "date_created", "direction": "DESC" } +}} + +// Step 3: List notifications to find the right one +{ "ability": "gravityforms/notifications-list", "params": { "form_id": 307 } } + +// Step 4: Resend specific notification to specific entry +{ "ability": "gravityforms/notifications-send", "params": { + "form_id": 307, + "entry_id": 663, + "notification_ids": ["6151b6f3c121b"] +}} +``` + +### Tips + +- If `notification_ids` is omitted, ALL active notifications for that form are sent — use `notifications-list` first to target the right one. +- `notifications-send` uses the **current** form notification config — if the notification was modified since the entry was created, the updated version is sent. +- Notifications respect conditional logic — if the entry doesn't match the notification's conditions, it won't send. + +## Data Availability — What GF Does NOT Track + +**Validation failures / form abandonment:** GF only stores entries on successful submission (all validation passes). Failed attempts — where a user hits Submit with empty required fields — are not recorded. There is no data on which fields cause the most validation failures, abandonment rates, or partial completion patterns. + +If a user asks "which required fields are people skipping?" or "why are we getting incomplete submissions?", explain that this data doesn't exist in GF. Suggest: +- The **Partial Entries** add-on (captures field data on page changes and form abandonment) +- Client-side analytics (JS-based field interaction tracking) +- `submissions-validate` for one-off dry-run validation of a specific input set (not historical data) diff --git a/skills/gravity-forms-abilities/references/field-config.md b/skills/gravity-forms-abilities/references/field-config.md new file mode 100644 index 0000000..45b1cfa --- /dev/null +++ b/skills/gravity-forms-abilities/references/field-config.md @@ -0,0 +1,372 @@ +# Field Configuration Reference + +## Compound Field Sub-Input Mappings + +Compound fields store data across multiple sub-inputs. Each sub-input ID is `{field_id}{suffix}`. + +### Name Field (`type: name`) + +| Suffix | Label | Visible by Default | +|---|---|---| +| `.2` | Prefix | No | +| `.3` | First | Yes | +| `.4` | Middle | No | +| `.6` | Last | Yes | +| `.8` | Suffix | No | + +**Required layout properties:** + +| Property | Value | Purpose | +|---|---|---| +| `nameFormat` | `"advanced"` | **Required.** Controls sub-input layout. `"advanced"` = First/Last side-by-side (standard behavior). Without this, sub-inputs render stacked. | +| `size` | `"large"` | Sets field width. The UI editor sets this by default. | + +⚠️ **If you omit `nameFormat`, the name field renders with First and Last stacked vertically instead of side-by-side.** The form editor UI auto-upgrades fields to `"advanced"` on open, but API-created fields without this property will render incorrectly until manually edited. + +Submission example (field ID 5): `{ "input_5.3": "John", "input_5.6": "Doe" }` + +### Address Field (`type: address`) + +| Suffix | Label | +|---|---| +| `.1` | Street Address | +| `.2` | Address Line 2 | +| `.3` | City | +| `.4` | State / Province | +| `.5` | ZIP / Postal Code | +| `.6` | Country | + +Submission example (field ID 3): `{ "input_3.1": "123 Main St", "input_3.3": "Springfield", "input_3.4": "IL", "input_3.5": "62701", "input_3.6": "United States" }` + +### Time Field (`type: time`) + +| Suffix | Label | +|---|---| +| `.1` | Hour | +| `.2` | Minute | +| `.3` | AM/PM | + +Submission example (field ID 7): `{ "input_7.1": "2", "input_7.2": "30", "input_7.3": "pm" }` + +## Choice Field Configuration + +Fields with `supports_choices: true` require a `choices` array of `{text, value}` objects: + +```json +{ + "type": "select", + "label": "Favorite Color", + "choices": [ + { "text": "Red", "value": "red" }, + { "text": "Blue", "value": "blue" }, + { "text": "Green", "value": "green" } + ] +} +``` + +Choice field types: `select`, `radio`, `checkbox`, `multiselect`, `image_choice` + +## Common Field Types + +| Type | Label | Choices | Compound | Key Capabilities | +|---|---|---|---|---| +| `text` | Single Line Text | No | No | placeholder, default value, conditional logic | +| `textarea` | Paragraph Text | No | No | conditional logic | +| `email` | Email | No | No | validates email format, conditional logic | +| `phone` | Phone | No | No | format validation, conditional logic | +| `number` | Number | No | No | range options, conditional logic | +| `date` | Date | No | No | date picker with format options | +| `select` | Drop Down | **Yes** | No | single selection, conditional logic | +| `radio` | Radio Buttons | **Yes** | No | single selection, conditional logic | +| `checkbox` | Checkboxes | **Yes** | No | multiple selection, conditional logic | +| `multiselect` | Multi Select | **Yes** | No | multiple selection, conditional logic, **pass values as array on submission** | +| `name` | Name | No | **Yes** | prefix/first/middle/last/suffix sub-inputs | +| `address` | Address | No | **Yes** | street/city/state/zip/country sub-inputs | +| `time` | Time | No | **Yes** | hour/minute/AM-PM sub-inputs | +| `hidden` | Hidden | No | No | not visible, good for tracking data | +| `website` | Website | No | No | URL validation, conditional logic | +| `fileupload` | File Upload | No | No | `allowedExtensions`, `maxFileSize`, `maxFiles` — see Type-Specific Config | +| `consent` | Consent | No | No | yes/no checkbox with description | +| `html` | HTML | No | No | display-only, no data collected | +| `section` | Section | No | No | visual separator, no data collected | +| `page` | Page | No | No | multi-page break, no data collected | + +Note: Always call `system-field-types` for the authoritative, up-to-date list — add-ons may register additional types. + +## Type-Specific Configuration + +`system-field-types` returns generic capability flags but does **not** expose type-specific properties. The following properties are accepted by `forms-create` and `forms-update` — pass them directly on the field object. + +### Name (`type: name`) + +| Property | Type | Description | +|---|---|---| +| `nameFormat` | string | **Required.** `"advanced"` = First/Last/Prefix/Middle/Suffix side-by-side (standard behavior). `"simple"` = single text input. Omitting this or using `"standard"` renders sub-inputs stacked vertically — incorrect for most use cases. | +| `size` | string | `"large"` (default in UI), `"medium"`, or `"small"`. Controls field width in form layout. | + +Example — standard name field with First and Last side-by-side: +```json +{ + "type": "name", + "label": "Full Name", + "isRequired": true, + "nameFormat": "advanced", + "size": "large", + "inputs": [ + { "id": "1.2", "label": "Prefix", "isHidden": true }, + { "id": "1.3", "label": "First" }, + { "id": "1.4", "label": "Middle", "isHidden": true }, + { "id": "1.6", "label": "Last" }, + { "id": "1.8", "label": "Suffix", "isHidden": true } + ] +} +``` + +**Note:** The `inputs` array is optional on creation — GFAPI populates default inputs for name fields. But if you need to control visibility (e.g., show Middle name), include the full array with `isHidden` flags. + +### File Upload (`type: fileupload`) + +| Property | Type | Description | +|---|---|---| +| `allowedExtensions` | string | Comma-separated list of allowed file extensions (no dots). Example: `"pdf,doc,docx"` | +| `maxFileSize` | integer | Maximum file size in megabytes. Example: `5` = 5MB | +| `maxFiles` | integer | Maximum number of files when multi-file upload is enabled. Omit or `0` for single-file mode. | +| `multipleFiles` | boolean | Enable multi-file upload. When `true`, users can upload multiple files up to `maxFiles`. | + +Example — PDF/Word only, 5MB max, single file: +```json +{ + "type": "fileupload", + "label": "Resume", + "isRequired": true, + "description": "Upload your resume (PDF or Word, max 5MB)", + "allowedExtensions": "pdf,doc,docx", + "maxFileSize": 5 +} +``` + +Example — multi-file with limit: +```json +{ + "type": "fileupload", + "label": "Supporting Documents", + "multipleFiles": true, + "maxFiles": 3, + "allowedExtensions": "pdf,jpg,png", + "maxFileSize": 10 +} +``` + +**Note:** File upload fields can be *configured* through abilities, but actual file uploads cannot be submitted via `submissions-submit` — the MCP transport does not support binary payloads. Files must be uploaded through the rendered form. + +### Number (`type: number`) + +| Property | Type | Description | +|---|---|---| +| `numberFormat` | string | `"decimal_dot"` (1,234.56), `"decimal_comma"` (1.234,56), or `"currency"` | +| `rangeMin` | number | Minimum allowed value | +| `rangeMax` | number | Maximum allowed value | + +### Date (`type: date`) + +| Property | Type | Description | +|---|---|---| +| `dateType` | string | `"datepicker"` (calendar popup) or `"datefield"` (manual entry) or `"datedropdown"` (dropdowns) | +| `dateFormat` | string | `"mdy"`, `"dmy"`, or `"ymd"` | +| `calendarIconType` | string | `"none"`, `"calendar"`, or `"custom"` | + +### Phone (`type: phone`) + +| Property | Type | Description | +|---|---|---| +| `phoneFormat` | string | `"standard"` (US: (###) ###-####) or `"international"` (free-form) | + +### Consent (`type: consent`) + +| Property | Type | Description | +|---|---|---| +| `checkboxLabel` | string | Text displayed next to the checkbox (the consent statement the user agrees to). Example: `"I consent to having this website store my submitted information."` | +| `description` | string | Longer explanatory text displayed below the checkbox. Example: `"We will only use your personal data to respond to your enquiry. See our Privacy Policy for details."` | + +Example — GDPR consent checkbox: +```json +{ + "type": "consent", + "label": "GDPR Consent", + "isRequired": true, + "checkboxLabel": "I consent to having this website store my submitted information so they can respond to my inquiry.", + "description": "We will only use your personal data to respond to your enquiry. See our Privacy Policy for details." +} +``` + +**Note:** `checkboxLabel` and `description` persist through GFAPI but are not exposed by `system-field-types` — the agent skill's field-config reference is the only source for these properties. + +### Text / Paragraph (`type: text`, `type: textarea`) + +| Property | Type | Description | +|---|---|---| +| `maxLength` | integer | Maximum character count | + +### Multiselect (`type: multiselect`) + +| Property | Type | Description | +|---|---|---| +| `storageType` | string | **Auto-set by abilities layer.** `"json"` stores values as a JSON array, preventing data loss when choice values contain commas. The abilities `forms-create` handler sets this automatically. | + +**Submission format:** Pass multiselect values as an **array**, not a comma-separated string: + +```json +{ + "input_values": { + "input_1": ["Atlanta, GA", "New York, NY"] + } +} +``` + +⚠️ **Never pass multiselect values as a comma-separated string** (e.g., `"Atlanta, GA,New York, NY"`). Values containing commas become ambiguous and cause data loss on round-trip. + +**Searching multiselect entries:** Use the `contains` operator to find entries with a specific selected value: +```json +{ + "field_filters": [{ "key": "1", "operator": "contains", "value": "Atlanta, GA" }] +} +``` +The `is` operator does not work for individual multiselect values — it matches the entire stored string. + +## Form Design Patterns + +### Contact Form + +```json +{ + "title": "Contact Us", + "fields": [ + { "type": "name", "label": "Your Name", "isRequired": true, "nameFormat": "advanced", "size": "large" }, + { "type": "email", "label": "Email Address", "isRequired": true }, + { "type": "phone", "label": "Phone Number" }, + { "type": "select", "label": "Subject", "choices": [ + { "text": "General Inquiry", "value": "general" }, + { "text": "Support", "value": "support" }, + { "text": "Sales", "value": "sales" } + ]}, + { "type": "textarea", "label": "Message", "isRequired": true } + ] +} +``` + +### Registration Form + +```json +{ + "title": "Event Registration", + "fields": [ + { "type": "name", "label": "Full Name", "isRequired": true, "nameFormat": "advanced", "size": "large" }, + { "type": "email", "label": "Email", "isRequired": true }, + { "type": "phone", "label": "Phone", "isRequired": true }, + { "type": "address", "label": "Address" }, + { "type": "radio", "label": "Attendance", "isRequired": true, "choices": [ + { "text": "In Person", "value": "in-person" }, + { "text": "Virtual", "value": "virtual" } + ]}, + { "type": "textarea", "label": "Special Requests" } + ] +} +``` + +### Feedback / Survey Form + +```json +{ + "title": "Customer Feedback", + "fields": [ + { "type": "radio", "label": "How satisfied are you?", "isRequired": true, "choices": [ + { "text": "Very Satisfied", "value": "5" }, + { "text": "Satisfied", "value": "4" }, + { "text": "Neutral", "value": "3" }, + { "text": "Dissatisfied", "value": "2" }, + { "text": "Very Dissatisfied", "value": "1" } + ]}, + { "type": "textarea", "label": "Additional Comments" }, + { "type": "hidden", "label": "Source", "defaultValue": "website" } + ] +} +``` + +## Layout Grid + +Fields default to full-width (12 columns). To place fields side-by-side, give them the same `layoutGroupId` and set `layoutGridColumnSpan` to control width. + +### Properties + +| Property | Type | Description | +|---|---|---| +| `layoutGroupId` | string | Row group identifier. Fields sharing the same value render on the same row. Use readable names — the server normalizes to internal format. | +| `layoutGridColumnSpan` | integer (1–12) | Number of grid columns the field spans. Default is 12 (full width). | + +### Common Span Patterns + +| Layout | Spans | Example | +|---|---|---| +| Two equal columns | `6 + 6` | First name / Last name | +| Three equal columns | `4 + 4 + 4` | City / State / Zip | +| Four equal columns | `3 + 3 + 3 + 3` | Quarter-width fields | +| Two-thirds / one-third | `8 + 4` | Address / Apt number | +| Three-quarters / one-quarter | `9 + 3` | Description / Priority | + +### Natural Language Mapping + +When users describe layout in natural language, map to these properties: + +| User says | Agent does | +|---|---| +| "side by side" / "next to each other" / "on the same row" | Same `layoutGroupId`, span `6` each | +| "three across" / "three columns" | Same `layoutGroupId`, span `4` each | +| "four across" / "four columns" | Same `layoutGroupId`, span `3` each | +| "full width" / "its own row" / "takes the whole row" | No `layoutGroupId`, span `12` or omit both | +| "make X narrower" / "small field" | Reduce span (e.g. `3` or `4`), group with adjacent field | +| "two-thirds / one-third split" | Spans `8` + `4` in same group | +| "three-quarters / one-quarter" | Spans `9` + `3` in same group | +| "equal width" | Same span for all fields in the group | +| "compact layout" / "space-efficient" | Group related fields into rows of 2–3 | + +### Examples + +**Two fields side-by-side (half-width each):** + +```json +{ + "fields": [ + { "type": "text", "label": "First Name", "layoutGroupId": "name-row", "layoutGridColumnSpan": 6 }, + { "type": "text", "label": "Last Name", "layoutGroupId": "name-row", "layoutGridColumnSpan": 6 } + ] +} +``` + +**Three fields in a row:** + +```json +{ + "fields": [ + { "type": "text", "label": "City", "layoutGroupId": "location", "layoutGridColumnSpan": 4 }, + { "type": "select", "label": "State", "layoutGroupId": "location", "layoutGridColumnSpan": 4, "choices": [{"text": "CA", "value": "CA"}] }, + { "type": "text", "label": "Zip", "layoutGroupId": "location", "layoutGridColumnSpan": 4 } + ] +} +``` + +**Mixed layout — two-thirds / one-third:** + +```json +{ + "fields": [ + { "type": "text", "label": "Street Address", "layoutGroupId": "addr", "layoutGridColumnSpan": 8 }, + { "type": "text", "label": "Apt/Suite", "layoutGroupId": "addr", "layoutGridColumnSpan": 4 } + ] +} +``` + +### Notes + +- Spans in a row don't need to sum to exactly 12 — the CSS grid wraps naturally. +- Fields without `layoutGroupId` render on their own row at full width. +- The server normalizes `layoutGroupId` to internal format; always use readable names like `"row1"`, `"contact-info"`, etc. diff --git a/skills/placeholder-skill/SKILL.md b/skills/placeholder-skill/SKILL.md deleted file mode 100644 index f8d97fb..0000000 --- a/skills/placeholder-skill/SKILL.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: placeholder-skill -description: A placeholder skill for testing the Gravity Skills repository framework. Not intended for production use. Use this to verify the skill validation, zip packaging, and distribution pipeline works correctly. -license: GPL-2.0+ -compatibility: Designed for Claude Code and compatible AI coding agents -metadata: - author: gravityforms - version: "0.0.1" ---- - -# Placeholder Skill - -> **This is a test skill.** It exists to validate the Gravity Skills repository framework and distribution pipeline. It does not provide any real functionality. - -This skill is used to verify: -- Skill validation (`./scripts/validate-skills.sh`) correctly parses SKILL.md frontmatter -- Zip packaging (`./scripts/pack-skills.sh`) correctly bundles skill directories -- GitHub Releases distribution works end-to-end - -## When to use - -Do not use this skill for real work. It is a framework validation fixture only. - -## References - -- [Framework validation notes](references/README.md) diff --git a/skills/placeholder-skill/references/README.md b/skills/placeholder-skill/references/README.md deleted file mode 100644 index 3a2e7e7..0000000 --- a/skills/placeholder-skill/references/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Placeholder Skill — References - -This directory exists to validate that the zip packaging script correctly includes subdirectories. - -If you are reading this inside a zip file, the packaging pipeline is working correctly.