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, ornullwhen it no longer exists.await shopify.files.delete(id)permanently deletes the file and returnsfalsewhen 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.
export default async ({ input, shopify }) => {
return shopify.files.createUpload({
filename: input.filename,
mimeType: input.mimeType,
fileSize: input.fileSize,
});
};{
"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.
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.
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 };
};{
"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.
createUploadrejects larger sizes before asking Shopify.fileSizeis what the browser reports; for a stricter limit, also checksizeonce the file isREADYand 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_recordsremoves test records but not the files they reference, so delete test files withshopify.files.delete. - Deleting is permanent and removes the file wherever the store uses it, including products and pages.