Skip to content
littleworksdocs

Explore the documentation

Meet LittleworksA little backend for what you’re building on Shopify.Littleworks or your own appWhat it takes to give a Shopify feature a backend: with Littleworks, or with a custom app you build and run yourself.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 a version you test and then publish.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.Store filesKeep uploads such as artwork and photos in the merchant’s Shopify Files, uploaded by the browser straight to Shopify.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.Back-in-stock alertsCollect confirmed signups on sold-out variants, email them through your own provider when stock returns, and see every signup and send in your admin.Transactional emailSend email from your works through your own provider, never twice for the same thing, and see what was sent and what failed.Build a merchant pageDescribe your records and what merchants do with them; Littleworks renders a native Shopify admin page.Run works on their ownRun a work when something happens in the store, on a schedule, or from a Shopify Flow workflow.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

Store files

Keep uploads such as artwork and photos in the merchant’s Shopify Files, uploaded by the browser straight to Shopify.

Files live in the merchant’s own Shopify Files (Content → Files in Shopify admin), not in Littleworks. The browser uploads a file directly to Shopify, so large bodies never pass through a work. A work only asks Shopify for an upload target and then adds the uploaded file to Files. Records keep the file’s ID.

  • await shopify.files.createUpload({filename, mimeType, fileSize}) returns {url, method, parameters, resourceUrl, contentType}: a one-time target the browser POSTs the file to.
  • await shopify.files.save({resourceUrl, contentType?, alt?, filename?}) adds the uploaded file to Files and returns it.
  • await shopify.files.get(id) returns the file’s current status and URL, or null when it no longer exists.
  • await shopify.files.delete(id) permanently deletes the file and returns false when it did not exist.

save and get return {id, type, status, url, alt, mimeType, size, width, height, createdAt, errors}. type is IMAGE, FILE or VIDEO, chosen from the MIME type: JPEG, PNG, GIF, WebP and HEIC are images; MP4, MOV and WebM are videos; any other type except HTML is a file. Shopify processes files after save, so status starts as UPLOADED or PROCESSING and url can be null until it is READY. A FAILED file lists why in errors.

Access

The helpers are ordinary Admin GraphQL calls made for you, so the work needs permissions.shopify: true, and the store must grant Files access in Littleworks → Agent access → Permissions. Uploading, saving and deleting need Edit (write_files); get needs viewing (read_files). Without it, the helpers throw SHOPIFY_FILES_ACCESS_REQUIRED with that instruction. Page data works can call get but not the others, since they only read.

Example: artwork for a quote request

The first work hands the browser an upload target. Its input schema sets the types and size the store accepts.

quotes.artwork-upload
export default async ({ input, shopify }) => {
  return shopify.files.createUpload({
    filename: input.filename,
    mimeType: input.mimeType,
    fileSize: input.fileSize,
  });
};
Manifest
{
  "name": "quotes.artwork-upload",
  "title": "Prepare an artwork upload",
  "feature": "quotes",
  "trigger": {
    "type": "http",
    "method": "POST",
    "auth": "public",
    "origins": [
      "https://your-store.example"
    ]
  },
  "permissions": {
    "shopify": true
  },
  "inputSchema": {
    "type": "object",
    "properties": {
      "filename": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "mimeType": {
        "enum": [
          "image/png",
          "image/jpeg",
          "application/pdf"
        ]
      },
      "fileSize": {
        "type": "integer",
        "minimum": 1,
        "maximum": 10000000
      }
    },
    "required": [
      "filename",
      "mimeType",
      "fileSize"
    ],
    "additionalProperties": false
  }
}

The browser sends the file to Shopify as a form: every returned parameter, then the file last, in a field named file. It then submits the request with the resourceUrl.

Upload from the storefront
const file = form.querySelector('input[type="file"]').files[0];
const post = async (url, body) => {
  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!response.ok) throw new Error("The request could not be completed");
  return response.json();
};

const target = await post(uploadWorkUrl, {
  filename: file.name,
  mimeType: file.type,
  fileSize: file.size,
});

const upload = new FormData();
for (const { name, value } of target.parameters) upload.append(name, value);
upload.append("file", file);
const sent = await fetch(target.url, { method: target.method, body: upload });
if (!sent.ok) throw new Error("The file could not be uploaded");

await post(submitWorkUrl, {
  name: form.elements.name.value,
  details: form.elements.details.value,
  artwork: { resourceUrl: target.resourceUrl, contentType: target.contentType },
});

The second work saves the file to Files and keeps its ID on the record. Its schema accepts only an HTTPS resourceUrl, and save also rejects anything that is not a Shopify upload target, so a visitor cannot make the store import a file from elsewhere.

quotes.submit
export default async ({ input, db, shopify }) => {
  const file = await shopify.files.save({
    resourceUrl: input.artwork.resourceUrl,
    contentType: input.artwork.contentType,
    alt: `Artwork for ${input.name}`,
  });

  const quote = await db.collection("quotes").create({
    name: input.name,
    details: input.details,
    artworkFileId: file.id,
    status: "new",
  });

  return { id: quote.id };
};
Input schema
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "details": {
      "type": "string",
      "maxLength": 2000
    },
    "artwork": {
      "type": "object",
      "properties": {
        "resourceUrl": {
          "type": "string",
          "pattern": "^https://",
          "maxLength": 2048
        },
        "contentType": {
          "enum": [
            "IMAGE",
            "FILE"
          ]
        }
      },
      "required": [
        "resourceUrl",
        "contentType"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "name",
    "artwork"
  ],
  "additionalProperties": false
}

To show the artwork, a page data work or another work calls shopify.files.get(quote.data.artworkFileId) and uses url once status is READY. Image and file URLs are on Shopify’s CDN, so a storefront or admin page can display or link to them directly. To render a file from Liquid instead, save its ID to a file_reference metafield and use Shopify’s image_url filter or the file’s url.

Limits and care

  • Shopify accepts images and other files up to 20 MB and videos up to 1 GB, and never HTML. createUpload rejects larger sizes before asking Shopify. fileSize is what the browser reports; for a stricter limit, also check size once the file is READY and delete files that exceed it.
  • Files are public to anyone with their URL. Do not use them for confidential documents.
  • Each helper call is one SDK call within the run’s limit. They do not count as external requests.
  • Test runs save real files to the store. delete_test_records removes test records but not the files they reference, so delete test files with shopify.files.delete.
  • Deleting is permanent and removes the file wherever the store uses it, including products and pages.