Skip to content
littleworksdocs

Explore the documentation

Meet LittleworksA little backend for what you’re building on Shopify.Connect to LittleworksChoose the CLI, a plugin, or a remote MCP connection.CLIConnect from your terminal with Shopify approval and no manual tokens.Claude pluginThe Littleworks MCP connection and agent guidance in one package.OpenAI pluginThe Littleworks MCP connection and agent guidance in one package.MCP with OAuthConnect a compatible client directly, without a marketplace plugin.Build with an agentThe operating guide for agents building on Shopify with Littleworks.Write a workA JavaScript handler, a small manifest, and an immutable version each time you deploy.Keep secretsEncrypted credentials, shared across your store’s works.Call external servicesMake public HTTPS API requests from a work using http.fetch().Store dataDocument collections with a small API, scoped automatically to the work that uses them.Call ShopifyCall Shopify’s Admin and Storefront GraphQL APIs with credentials held by Littleworks.Expose an endpointConnect an existing frontend to a work through a small JSON API.Customer access and invitationsRequire a signed-in shopper or a narrow, expiring invitation before a work runs.RecipesUseful things to build for your store, with the backend already taken care of.Product reviewsCollect customer reviews, verify purchases, and publish approved content directly into your Shopify theme.Customer wishlistsGive signed-in customers a persistent list of products they can revisit across devices.Customer quote requestsCollect a customer’s products, quantities, and requirements without turning a request into an order.Build a merchant viewCombine data and action works in a native Shopify screen, with draft and published versions.MCP tool referenceTools to inspect, deploy, run, and organize the backend for a connected store.Usage and allowancesSee what your works use, how much capacity remains, and when allowances reset.Inspect and troubleshootFind the relevant run, understand the failure, and make the next change deliberately.Limits and securityThe current runtime boundaries, data isolation model, and execution allowances.
Shopify admin
Build
Markdown

Build a merchant view

Combine data and action works in a native Shopify screen, with draft and published versions.

Views are saved screens inside Littleworks → Views. Your agent defines a bounded JSON layout and dedicated works that load data or perform merchant actions. Littleworks renders standard Polaris components. One view can combine several data works, and one work result can populate several components. Collections stay schemaless; data can also come from Shopify or an external service.

Draft and published

RevisionAgent / CLIShopify admin
Unpublished work revisionCan invoke for authorized testingCan run through the draft view
Published work revisionCannot invokeCan run through its current draft or published view binding

A view has one draft pointer and an optional published pointer. Saving updates the draft only. Publishing atomically activates that definition and marks its exact data/action work revisions published. Later work deployments and draft edits leave the published view unchanged. To test a change, deploy a new work revision and bind it in a new draft. Previously published revisions stay unavailable to direct agent invocation, even if the view is replaced or removed. A run admitted before publication can finish.

Create a view

  1. Read get_context, list_functions and list_views. Inspect existing drafts with get_view before editing.
  2. Deploy dedicated works with trigger:{type:"view",view:"VIEW_NAME",role:"data"} or role:"action". A work’s owning view and role are fixed. Each work keeps its own data group and environment.
  3. Test unpublished revisions with invoke_function, supplying name, environment, version, input and a fresh idempotencyKey. For a data work, include filters:{}, search:"", sort:null, after:null and limit:25 plus any saved source inputs. Inspect failed runs before retrying actions.
  4. Save with deploy_view({definition,baseVersion}). Use null only for a new view; otherwise use the draft version returned by get_view. Bind the exact work revision UUIDs you tested.
  5. Open manageUrl to preview the draft in Shopify. Verify independent loading, filters, pagination, record details, forms, stale-record handling and the intended outcomes.
  6. When the merchant requests publication, call publish_view({name,version,publishedVersion}) using both current pointers from get_view, or publish inside Shopify. Open the published view and verify it.

Data works

sources declares up to eight named bindings: {id,work,version,input?,filters?,search?,sorts?}. A form or informational view can have no sources. Each source invokes its pinned data work on page load. Search submissions, filters, sorting, pagination, Refresh, and a successful action’s refresh list reload the affected sources. Sources load independently; a failed source shows its error and run link without preventing other sources from displaying. Metadata reads, including get_view, do not execute works. Data loads and actions use ordinary run allowances, limits, logs and idempotency.

Data-work handler
export default async ({input, db}) => {
  const page = await db.collection('requests').list({
    limit: input.limit,
    ...(input.after ? {after: input.after} : {}),
    ...(input.filters.status ? {where: {status: input.filters.status}} : {}),
  });
  return {
    items: page.items.map(record => ({
      ...record.data, id: record.id, revision: record.revision,
      createdAt: record.createdAt,
    })),
    nextCursor: page.nextCursor,
  };
};

Input is saved source input plus filters (selected typed values), search (a string, empty initially), sort ({field,direction} or null), after (an opaque cursor or null), and limit:25. These five keys are reserved; declare them in the work inputSchema. Source filters support up to three selects with {field,label,options:[{label,value}]}. Enable search with search:{label}; declare sorts as [{id,label,field,direction:"asc"|"desc"}]. The first sort is the default. Littleworks validates choices; the work implements the actual query. Search runs on submission, not each keystroke. Filters, search and sorting reset pagination. A cursor must remain tied to the same query. Bound scans can produce an empty page with a next cursor; never discard matching records when filling a page.

Return a JSON object. Records needs items (at most 25 objects, each with a unique string id) and an optional string/null nextCursor. Details reads fields from the returned object, so a separate summary work can return {pending:12,averageRating:4.7}. Field paths support nested own properties such as totals.pending; they are not expressions. Return only data the merchant should see, and keep the work result within 64 KiB. Do not return credentials.

The runtime limits data works to document reads, Shopify GraphQL queries, secret reads and external GET/HEAD requests. Document writes, uniqueness changes, GraphQL mutations/subscriptions, invitation issuance and mutating HTTP methods are blocked, including in agent tests. GET/HEAD requests still contact real services: use endpoints designed for reading. This policy does not prove an external service is free of side effects. Business changes belong in action works.

Component catalog

Use apiVersion:3. name is a stable identifier; title is merchant-facing. layout:{type:"stack"|"details",main:[elementId,...],aside?:[elementId,...],elements:{...}} places a vertical stack in the main column. Only details pages may have a small supporting sidebar. Littleworks owns the page header, breadcrumb and navigation. Do not build an app shell, nested navigation or custom grids. pageActions:[actionId,...] places up to three page actions in the native header.

ComponentConfiguration
Sectionheading and children. Groups Text, Details, Form or ResourceList. Renders as a card on pages and an unboxed heading and content in modals. Sections cannot nest.
Texttext and optional subdued. Plain escaped content.
Detailssource and fields. Label/value pairs from a data work.
Recordsheading, source, columns, onRowClick (modal link) or details/actions, bulkActions and empty:{heading,message,action?}. Search/filter/sort controls come from the source. Place directly in the main column.
ResourceListsource, field (default items), titleField, optional subtitleField/imageField/resourceField, onRowClick or details/actions, and empty. Put inside a Section.
Metricsheading, source and up to four metrics:{field,label,format?,comparisonField?,comparisonLabel?}. Place directly in the main column.
Calloutheading, message and optional page action. A card on pages; unboxed in modals.
EmptyStateheading, message and optional page action. A card on pages; unboxed in modals.
Formaction. Put inside a Section; the referenced page action declares its fields and input bindings.

Use at most 40 components and 20 main-column items per page or modal; details pages also allow six sidebar items. Every component must be placed once. No arbitrary frontend code, HTML, CSS, URLs, expressions or footer-help components. Work results remain data, never layout instructions. Multiple components may share one source.

Fields use {field,label,format?}. Formats: text, number, date (UTC), badge, boolean, money, resource and list. Money reads {amount,currencyCode}; badge mappings use {value,label,tone}. A resource reads {id,title,subtitle?,image?}, with a Shopify Product, Customer or Order GID. Littleworks builds the admin link itself; images must be HTTPS URLs on cdn.shopify.com. list displays up to 25 resources or text values. Other objects display as escaped JSON. A table supports six columns and 20 detail fields. Rows can open a data-driven modal using onRowClick, show snapshot details using details/actions, or be read-only when neither is declared. Selection controls are separate when bulk actions are present.

ResourceList reads an array of at most 25 objects from its field path. Rows use titleField and optional subtitleField/imageField. resourceField can link a Shopify resource when there are no detail fields. Record actions require field:"items", unique string IDs and details; pagination uses nextCursor when field is items. Metrics accepts number or money formats. Comparison fields are labels supplied by the data work; Littleworks does not calculate comparisons or infer whether a change is good.

Modals with their own data

Define up to four modals in modals:[{id,title,inputs,sources,actions,pageActions,layout}]. Each modal has its own source/action IDs and uses a single-column stack of catalog components. It does not create another page, sidebar or navigation shell. A modal can use several independent data works; sources load only when opened and use normal run allowances. Closing unmounts the dialog; reopening loads fresh data. Loading errors offer an explicit retry. Nested modal links are not supported.

inputs declares up to eight required scalar inputs, for example {reviewId:"string"}. Types are string, number and boolean; strings are bounded to 1000 characters. Modal source inputBindings maps work input keys to modal input names, for example {reviewId:"reviewId"}. These values merge with saved source input constants and the standard filters/search/sort/after/limit arguments. The work inputSchema validates the assembled input. Sources cannot depend on other source results.

A table opens a modal with a verified row ID
{
  "modalLinks": [
    {
      "id": "openReview",
      "label": "Open review",
      "modal": "reviewDetails",
      "scope": "row",
      "source": "records",
      "input": {
        "reviewId": {
          "source": "row",
          "field": "id"
        }
      }
    }
  ],
  "tableProps": {
    "source": "records",
    "onRowClick": "openReview"
  },
  "modal": {
    "id": "reviewDetails",
    "title": "Review details",
    "inputs": {
      "reviewId": "string"
    },
    "sources": [
      {
        "id": "review",
        "work": "views.reviews.detail",
        "version": "DEPLOYMENT_UUID",
        "inputBindings": {
          "reviewId": "reviewId"
        }
      }
    ]
  }
}

Declare modalLinks on the page with id, label, modal, scope (row or page), optional source, and input bindings. Records.onRowClick and ResourceList.onRowClick reference a row link on the same source; resource lists must use items with IDs. A page link can appear in pageActions, a Callout or an EmptyState. Page links use constants, or data bindings from their declared page source; row links use row fields or constants. Supply exactly the modal’s declared inputs. These are saved definitions; returned rows never choose works or supply UI instructions.

The server binds row/data modal inputs to the merchant, view version, stage and opening link in encrypted context that expires after 15 minutes. Arbitrary client-supplied IDs are not accepted. On expiry, close the dialog and refresh the page. Modal actions with data-bound inputs are also tied to the opened modal context. A fresh modal load does not lock a record; action works must still re-read state and check revisions.

Inside the modal, use Details, ResourceList, Records and other catalog components with local source IDs. Modal content is always unboxed: Section, Records, Metrics, Callout and EmptyState keep their content and headings without card borders, backgrounds or shadows. This is enforced by the renderer and cannot be overridden in a view definition. pageActions places action buttons in the native modal footer. A modal supports one Form; its submit/discard controls also appear in that footer. Action forms and record details reuse the current dialog instead of stacking dialogs. Unsaved edits are preserved until saved or explicitly discarded.

Modal actions use the same page/record/bulk scopes. A page action can bind the loaded modal source using {source:"data",field:"id"}, or a verified modal input with {source:"input",field:"reviewId"}. refresh targets local modal sources; refreshPage targets page sources. closeModal:true closes after success; otherwise the dialog remains open. Saving never automatically replays an action. Modal works use the parent view’s ownership and draft/published authorization rules.

Forms, record actions and bulk actions

Actions declare id, label, work, version, scope (page, record or bulk), optional source, input bindings, fields, confirmation, destructive, when:{field,equals} and refresh. Bind dedicated action works owned by this view. Records.actions and ResourceList.actions reference record actions on the same source. Records.bulkActions references bulk actions. Page actions can appear in a Form, Callout, EmptyState or pageActions. They can run without a data source, or bind a source for settings and summary actions. Work revisions stay pinned; pausing a work blocks every bound revision.

BindingUse
{source:"row",field:"id"}Record action: read a server-returned row field, including id or revision.
{source:"data",field:"revision"}Page action with source: read a field from the whole data-work result.
{source:"selection",fields:{id:"id",revision:"revision"}}Bulk action: receive an array of these fields for up to 25 selected records on the current page.
{source:"input",field:"reviewId"}Modal action: read a verified input supplied when opening the dialog.
{source:"form",field:"fieldName"}Read a validated declared form field.
{source:"constant",value:scalar}Use a constant from the saved definition.

The backend evaluates when on the source snapshot and encrypts bound row/data/selection context for that merchant, view version, stage and action. Context expires after 15 minutes. Browsers cannot replace bound identifiers or revisions; bulk selections must be distinct. Availability is a snapshot, not a lock. The action work inputSchema validates the final assembled input. A bulk selection invokes one work, not a separate work per row.

Form fields use name, label, type (text, textarea, select, number, money, checkbox or date), required, help, defaultValue and optional initialField. Text supports maxLength up to 4000. Select declares options:[{label,value}]. Number and money support min, max and step; money requires a three-letter currency. Date submits YYYY-MM-DD. Number/money submit numbers and checkbox submits a boolean. Empty optional number/date/select values are omitted. initialField reads the bound row or page result and is unavailable for bulk actions. Up to 12 fields per action and eight actions per page or modal. Validation runs in the UI and on the server.

Form renders standalone fields with save/discard controls. Record and page buttons open native modals with actions in the modal footer. Opening a form does not execute a work. Label customer-visible content and internal notes clearly. Dirty inline forms disable source refresh and list controls to preserve edits. refresh:[sourceId,...] reloads affected sources after success; the default is the bound action source, if present. Actions never retry automatically. Duplicate submission keys are rejected. Return an HTTP error or throw on failure; merchant feedback contains a bounded error and run link.

Example: reviews with independently loaded details

deploy_view arguments
{
  "baseVersion": null,
  "definition": {
    "apiVersion": 3,
    "name": "reviews",
    "title": "Reviews",
    "actions": [],
    "layout": {
      "type": "stack",
      "main": [
        "summary",
        "table"
      ],
      "aside": [],
      "elements": {
        "table": {
          "type": "Records",
          "props": {
            "columns": [
              {
                "field": "displayName",
                "label": "Customer",
                "format": "text"
              },
              {
                "field": "rating",
                "label": "Rating",
                "format": "number"
              },
              {
                "field": "status",
                "label": "Status",
                "format": "badge",
                "badges": [
                  {
                    "value": "pending",
                    "label": "Pending",
                    "tone": "warning"
                  },
                  {
                    "value": "approved",
                    "label": "Published",
                    "tone": "success"
                  },
                  {
                    "value": "rejected",
                    "label": "Rejected",
                    "tone": "neutral"
                  },
                  {
                    "value": "withdrawn",
                    "label": "Withdrawn",
                    "tone": "neutral"
                  }
                ]
              },
              {
                "field": "createdAt",
                "label": "Submitted",
                "format": "date"
              }
            ],
            "source": "records",
            "heading": "Reviews",
            "empty": {
              "heading": "No matching records",
              "message": "Try another filter, or return when new submissions arrive."
            },
            "onRowClick": "openDetails"
          }
        },
        "summary": {
          "type": "Metrics",
          "props": {
            "heading": "Latest submissions",
            "source": "summary",
            "metrics": [
              {
                "field": "sampleSize",
                "label": "Recent reviews"
              },
              {
                "field": "pending",
                "label": "Awaiting review"
              },
              {
                "field": "averageRating",
                "label": "Average rating"
              }
            ]
          }
        }
      }
    },
    "sources": [
      {
        "id": "records",
        "work": "views.reviews.list",
        "version": "00000000-0000-4000-8000-000000000002",
        "filters": [
          {
            "field": "status",
            "label": "Status",
            "options": [
              {
                "value": "pending",
                "label": "Pending"
              },
              {
                "value": "approved",
                "label": "Published"
              },
              {
                "value": "rejected",
                "label": "Rejected"
              },
              {
                "value": "withdrawn",
                "label": "Withdrawn"
              }
            ]
          }
        ],
        "search": {
          "label": "Search reviews"
        },
        "sorts": [
          {
            "id": "newest",
            "label": "Newest first",
            "field": "$createdAt",
            "direction": "desc"
          },
          {
            "id": "oldest",
            "label": "Oldest first",
            "field": "$createdAt",
            "direction": "asc"
          }
        ]
      },
      {
        "id": "summary",
        "work": "views.reviews.summary",
        "version": "00000000-0000-4000-8000-000000000003"
      }
    ],
    "modalLinks": [
      {
        "id": "openDetails",
        "label": "Open review",
        "modal": "details",
        "scope": "row",
        "source": "records",
        "input": {
          "reviewId": {
            "source": "row",
            "field": "id"
          }
        }
      }
    ],
    "modals": [
      {
        "id": "details",
        "title": "Review details",
        "inputs": {
          "reviewId": "string"
        },
        "sources": [
          {
            "id": "detail",
            "work": "views.reviews.detail",
            "version": "00000000-0000-4000-8000-000000000004",
            "inputBindings": {
              "reviewId": "reviewId"
            }
          }
        ],
        "actions": [
          {
            "id": "publish",
            "label": "Publish review",
            "work": "views.reviews.moderate",
            "version": "00000000-0000-4000-8000-000000000001",
            "input": {
              "id": {
                "source": "data",
                "field": "id"
              },
              "revision": {
                "source": "data",
                "field": "revision"
              },
              "decision": {
                "source": "constant",
                "value": "approve"
              }
            },
            "when": {
              "field": "status",
              "equals": "pending"
            },
            "confirmation": "Publish this review on the product page?",
            "scope": "page",
            "source": "detail",
            "refreshPage": [
              "records",
              "summary"
            ],
            "refresh": [
              "detail"
            ],
            "closeModal": false
          },
          {
            "id": "reject",
            "label": "Reject review",
            "work": "views.reviews.moderate",
            "version": "00000000-0000-4000-8000-000000000001",
            "input": {
              "id": {
                "source": "data",
                "field": "id"
              },
              "revision": {
                "source": "data",
                "field": "revision"
              },
              "decision": {
                "source": "constant",
                "value": "reject"
              }
            },
            "when": {
              "field": "status",
              "equals": "pending"
            },
            "confirmation": "Reject this review? It will remain in your records.",
            "destructive": true,
            "scope": "page",
            "source": "detail",
            "refreshPage": [
              "records",
              "summary"
            ],
            "refresh": [
              "detail"
            ],
            "closeModal": false
          }
        ],
        "pageActions": [
          "publish",
          "reject"
        ],
        "layout": {
          "type": "stack",
          "main": [
            "section"
          ],
          "elements": {
            "section": {
              "type": "Section",
              "props": {
                "heading": "Review"
              },
              "children": [
                "detail"
              ]
            },
            "detail": {
              "type": "Details",
              "props": {
                "source": "detail",
                "fields": [
                  {
                    "field": "displayName",
                    "label": "Customer",
                    "format": "text"
                  },
                  {
                    "field": "productId",
                    "label": "Product",
                    "format": "text"
                  },
                  {
                    "field": "rating",
                    "label": "Rating",
                    "format": "number"
                  },
                  {
                    "field": "body",
                    "label": "Review",
                    "format": "text"
                  },
                  {
                    "field": "status",
                    "label": "Status",
                    "format": "badge",
                    "badges": [
                      {
                        "value": "pending",
                        "label": "Pending",
                        "tone": "warning"
                      },
                      {
                        "value": "approved",
                        "label": "Published",
                        "tone": "success"
                      },
                      {
                        "value": "rejected",
                        "label": "Rejected",
                        "tone": "neutral"
                      },
                      {
                        "value": "withdrawn",
                        "label": "Withdrawn",
                        "tone": "neutral"
                      }
                    ]
                  }
                ]
              }
            }
          }
        }
      }
    ]
  }
}

Manage views

MCP toolCLI command
list_viewslittleworks views
get_view {name,stage:"draft"|"published"}littleworks view-get <name> --stage draft
deploy_view {definition,baseVersion}littleworks view-deploy deployment.json
publish_view {name,version,publishedVersion}littleworks view-publish <name> <draft-version> <published-version|null>
delete_view {name,version,publishedVersion}littleworks view-delete <name> <draft-version> <published-version|null>
invoke_function {name,environment,version,input,idempotencyKey}littleworks invoke <name> <input-json> --env preview --revision <uuid>

Saving, publishing and removal check the current version pointers to reject stale edits. Removing a view retains historical definitions, works, documents and runs. Remove a work from both current view versions (or remove the view), then pause it before deleting it. There is a limit of 100 registered views per store and 32 KiB per definition. Views inherit the store’s authenticated Littleworks app access; there are no per-view staff roles. Existing CLI versions can call every tool using littleworks call; newer command aliases require an updated CLI package.