All skills
wix avatar

/wix-app

@5382952 official
by Wix.comwix/skills33 stars
33

Build and review Wix CLI app extensions — dashboard pages, modals, plugins, menu plugins, custom element widgets, Editor React components, site plugins, embedded scripts, backend APIs, backend events, service plugins, data collections, and App Market readiness. Use when building ANY feature or extension for a Wix CLI app or preparing a Wix app for App Market review. Triggers on: add, build, create, implement, help me, dashboard, widget, plugin, backend, API, event, collection, embedded script, service plugin, Editor React component, checkout, shipping, tax, discount, SPI, CMS, schema, tracking, popup, admin panel, menu item, modal, validate, test, verify, register extension, App Market, app review, submission readiness.

Use this Skill: https://skilld.dev/gh/wix/skills/wix-app

This session only. Nothing lands on disk.

referencesdashboard-pageQUERY_AND_PAGING.md

≈2.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Querying and paging a Wix API from a collection page

DATA_SOURCES.md covers finding the method and its fields; this page is about calling it.

Filters are written in WQL, which is shared across the platform — the rules below come from About the Wix API Query Language, not from one endpoint's behaviour, so they hold for @wix/ecom, @wix/stores, @wix/bookings and the rest alike.

The filterable fields are a closed list, published per endpoint

"This endpoint declares which fields it can filter and sort by, and with which operators; that list is closed and covers this endpoint only. Anything else errors or silently returns the wrong rows." — WQL

So a field path is never inferable from the entity shape, and the endpoint's own prose is not authoritative either. Most query endpoints publish a "Supported Filters and Sorting" page, and the SDK typedoc for the query's filter field links to it — Contacts, Orders, Products V3, Pipelines and Bookings all do. Find the link before you guess:

grep -rho "https://[^)]*supported-filters[^)]*" node_modules/@wix/<pkg>/build/es/*.d.mts | sort -u

Read the page, don't just find it. It is a table of field → operators, and it routinely lists more than the endpoint's prose implies — Query Extended Bookings' prose mentions filtering courses "by scheduleId", while its table also declares serviceId and the staff resource.id on both bookedEntity branches, and contactDetails.contactId / .email. Those are the difference between a page with two working filters and one with five.

Honour the Filter Performance note. Many pages carry one, naming fields to include in every request — Query Extended Bookings "strongly recommends" startDate in all of them. Don't satisfy it by hiding a clause in fetchData: a filter the user cannot see or clear makes rows go missing for no visible reason. Seed the visible filter instead, so the default is applied, labelled and adjustable:

const dateFilter = dateRangeFilter({
  name: 'Date',
  initialValue: { from: startOfDay(subDays(new Date(), 30)), to: null },
});

initialValue is on every filter factory (FilterStateBaseParams), so the same trick seeds a status or category default. Pick a window that matches the page's job — a booking review wants recent and upcoming, an audit log wants the last 24 hours.

A seeded default must not defeat search. Search is a "find this anywhere" gesture, so a window the user never chose silently hiding matches from it reads as broken search — a measured run shipped exactly that, and the report was "search doesn't work", not "the date filter is too narrow". Bypass the range while it is still the seeded value, and keep it once the user has set their own:

const DEFAULT_FROM = startOfDayDaysAgo(30);
const isUntouchedDefault = (r?: { from?: Date; to?: Date }) =>
  !r?.to && r?.from?.getTime() === DEFAULT_FROM.getTime();
// in fetchData AND fetchTotal:
dateRange: search?.trim() && isUntouchedDefault(range) ? undefined : range,

Whatever a filter or a permission removes from the result, say so in noResultsState — "no matches" and "no matches because a default you didn't set, or a scope you don't hold, excluded them" look identical, and only the second is actionable.

Two traps the list resolves, both of which look like a working filter:

Nested paths. A field that reads as top-level on the entity is often only filterable at its full path. Query Extended Bookings describes itself as filtering "by scheduleId of the relevant service", and a bare scheduleId is rejected outright — the accepted paths are nested under bookedEntity. The error names the offending path exactly, so one call settles it:

{ "code": "INVALID_FILTER", "data": { "unknownField": { "fieldPath": "scheduleId" } } }

Oneof branches. When the entity has a oneof, one path covers only one branch, and a filter over just that branch silently drops every row of the other kind — exactly as the row mapper has to handle both. Cover them with $or, and check each branch separately before wiring it into a page:

$or: [
  { 'bookedEntity.item.slot.scheduleId':     { $in: scheduleIds } },
  { 'bookedEntity.item.schedule.scheduleId': { $in: scheduleIds } },
]

One operator per field

"The filter is written in WQL, where each field takes a single operator, so conditions are combined with the logical operators instead: $and and $or take an array of expressions, $not takes one, and they can nest. They are WQL syntax, not field capabilities." — WQL

A date range written the obvious way therefore fails, and the error quotes the whole object back as the offending "operator", which reads like a parser bug and is actually the rule:

{ startDate: { $gte: from, $lte: to } }   // INVALID_FILTER — unknownOperator

Ranges are two clauses under $and; $or nests inside it:

{ $and: [
  { status: { $in: statuses } },
  { startDate: { $gte: from } },
  { startDate: { $lte: to } },
  { $or: [ /* the oneof branches */ ] },
] }

Build the filter as a list of clauses and wrap it at the end — return the bare clause when there is only one, {} when there are none — rather than mutating one object and hoping the operators do not collide. A page with several filters hits this the moment two of them apply at once, which is usually after the single-filter case has already been called working. The full operator set — including $not, $nin, $exists, $isEmpty, $hasAll, $hasSome — is in the WQL article.

A follow-up cursor page carries the cursor alone

The cursor already encodes the filter and sort of the query that produced it, so re-sending them is rejected. This is not one API's quirk: it is stated on the cursorPaging.cursor field of every generated Wix SDK that offers cursor paging — 46 of the 179 auto_sdk_* packages in one install — in a standard sentence you can read before making a request:

"Cursor token pointing to a page of results. Not used in the first request. Following requests use the cursor token and not filter or sort."

Build the first request and the follow-ups differently:

const response = await ns.query(
  query.cursor
    ? { cursorPaging: { limit: query.limit, cursor: query.cursor } }
    : { filter, sort, cursorPaging: { limit: query.limit } },
);

Sending them together answers "Invalid cursor. Sort or filter can not be specified together with cursor" — and because the collection retries, that renders as a spinner under the last row, which reads as "still loading". What you return on the last page is the collection's own contract, and has its own trap: TABLE_STATE.md.

Cursor mode also takes a separate fetchTotal, since a cursor-paged response carries no total. Build its filter exactly as the page's, or the count disagrees with the rows it counts.

What fetchTotal is allowed to call

fetchTotal: (query) => Promise<number>. It has to resolve a number from something that counts — and the obvious-looking source is not one. A cursor-paged response carries no total, so pagingMetadata.total is undefined — and undefined reaches the collection as 0. @wix/data spells the rule out on PagingMetadataV2.total: "Returned if offset paging is used, returnTotalCount is true in the request, and tooManyToCount is false." Read the field's own doc comment in the SDK you are calling rather than assuming it is populated. That is the whole of the "SummaryBar reads 0 while the table under it is full of rows" bug, and it survives review because nothing throws and nothing fails to compile.

Use a real count, in this order:

// 1. A CMS collection: `items.query(id)` returns a builder whose `.count()` is
//    `Promise<number>` and honours the filter you put on it. Apply the page's
//    filter with the same function fetchData uses, so the two cannot drift.
//    (The standalone `items.count(id)` is marked @internal and takes no filter.)
const count{Feature} = (q: {Feature}Query): Promise<number> =>
  apply{Feature}Filter(items.query(COLLECTION_ID), q).count();

// 2. A vertical SDK with its own count endpoint: call that — it already returns a number.

// 3. Offset paging only — ask for the count, then read it back:
const { pagingMetadata } = await ns.query({ paging: { limit, offset }, returnTotalCount: true });
return pagingMetadata?.total ?? 0; // `?? 0` is honest HERE: you asked for the count

If the API offers no count at all, omit fetchTotal. collection.total then falls back to the rows loaded so far, which is a real number you can label ("50 shown") instead of a confident 0. Never paper over the gap with ?? 0 on a field that is simply absent.

When a table will not settle

An extension runs in a cross-origin iframe whose console you cannot read, but its own SummaryBar will happily display state.collection.status.status, a fetch counter and the last rejection message. That is what turns "it spins" into a named error in one reload.

Source: SKILL.md on GitHub

1 warningtoday4 checks · Risk SAFE
  • Gen Agent Trust Hubtoday

    This skill is a specialized development toolkit for building extensions on the Wix platform. It provides comprehensive instructions for creating dashboard pages, backend APIs, and site plugins using the Wix CLI and SDKs. No malicious patterns were detected; the skill's behaviors, such as dependency management, command execution for builds, and local script execution for code reviews, are entirely consistent with its purpose as a developer productivity tool for the Wix ecosystem.

  • Sockettoday

    1 alert: gptAnomaly

  • Snyktoday

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 5382952. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 20 minutes ago.

Activeupdated 10 hours ago
compatibility
requires `@wix/cli` >= 1.1.192.

README badge

README badge for wix/skills/wix-app

Builds dashboard pages, modals, plugins, custom widgets, Editor React components, backend APIs, events, service plugins, and data collections for Wix CLI apps. Provides decision logic, API patterns, and validation workflows; scaffolding is owned by the Wix CLI via `wix generate --params`.

Generated from the current SKILL.md.

What extension types does this skill cover?
All Wix CLI app extension types: dashboard pages, modals, plugins, menu plugins, custom element widgets, Editor React components, site plugins, embedded scripts, backend APIs, backend events, service plugins, and data collections.
Does this skill scaffold the extension files for me?
The Wix CLI owns scaffolding via `wix generate --params` for all extension types except Backend API. This skill provides decision logic, API guidance, and business-logic patterns to fill in the generated stubs. Backend API files must be created manually.
What Wix CLI version is required?
The skill requires @wix/cli >= 1.1.192.
Do I need to create a Data Collection extension for app-specific data?
Yes, if you're saving or persisting app-specific data, managing domain entities in a dashboard, or running a service plugin that reads app-configured data. The skill infers this automatically—you don't need to explicitly request it.
Does this skill cover Wix Stores API usage?
Yes. When using any Wix Stores API (products, inventory, orders), the skill requires dual V1/V3 catalog support and references the Stores Versioning guide for module selection and field mapping.

Generated from the current SKILL.md. These answers refresh after source changes.