Email Attachments Read and delete email attachment files captured by email-trigger workflows. These methods are available in Email Trigger (/triggers/email-trigger/) workflows. When an email arrives with file attachments, each attachment descriptor in data.attachments includes a key. Use that key to retrieve and clean up the file in a later retryable step. Attachment descriptor shape { filename: "invoice.pdf", contentType: "application/octet-stream", key: "shop-name/workflow-id/email-/invoice.pdf" } api.getAttachment(key) Retrieves an attachment by its storage key and returns the raw file content. const buffer = await api.getAttachment(key); Parameters | Parameter | Type | Description | | key | string | The key value from data.attachments[n].key | Returns Promise Returns an ArrayBuffer containing the raw file bytes, or null if the key does not exist or has expired. Example async processAttachment(data, headers, api) { const buffer = await api.getAttachment(data.key); if (!buffer) { console.log('Attachment not found or expired:', data.key); return; } console.log(Got ${buffer.byteLength} bytes: ${data.filename}); // Work with the buffer const blob = new Blob([buffer], { type: data.contentType }); const text = new TextDecoder().decode(buffer); // if text-based } api.getAttachment() returns the ArrayBuffer directly. Do not read a .buffer property from its result. api.deleteAttachment(key) Deletes an attachment from storage. Safe to call even if the key no longer exists. It is a no-op in that case. await api.deleteAttachment(key); Parameters | Parameter | Type | Description | | key | string | The key value from data.attachments[n].key | Returns Promise Example async processAttachment(data, headers, api) { const buffer = await api.getAttachment(data.key); if (!buffer) return; // ... process the file ... // Clean up once done, don't leave files in storage indefinitely await api.deleteAttachment(data.key); } Attachment retention Attachments are stored for 7 days from when the email was received. After that, api.getAttachment() returns null for those keys. Plan your workflow so that any attachment processing happens well within that window. Attachment size guidance This page does not define a hard attachment size limit. However, api.getAttachment() reads the full file into an ArrayBuffer, so very large attachments are still a poor fit for normal step processing. Use attachments for small to moderate files. If a workflow needs to handle large CSV-style inputs, move quickly into a chunked processing pattern and never pass raw attachment buffers between steps. The retryable step pattern The start step is not retryable. If an error occurs mid-execution, start does not run again, which means you cannot re-fetch the attachment there safely. The correct pattern is to pass the key string (not the file content) into a subsequent step: export class Workflow { async start(data, headers, api) { // Pass keys to retryable steps, do NOT read file content here for (const att of data.attachments) { await api.scheduleNextStep({ delay: 10, action: 'handleFile', payload: { key: att.key, filename: att.filename, contentType: att.contentType } }); } } async handleFile(data, headers, api) { // This step is retryable, safe to call api.getAttachment() here const buffer = await api.getAttachment(data.key); if (!buffer) { console.log('File unavailable:', data.key); return; } // ... process and then clean up ... await api.deleteAttachment(data.key); } } Pass only the key into the scheduled payload. Do not pass the full attachment buffer between steps. Note: api.getAttachment() and api.deleteAttachment() are only available in email-trigger workflows. Calling them in other trigger types throws an error. Caution: api.getAttachment() and api.deleteAttachment() are not available inside onWorkflowComplete or onWorkflowError lifecycle hooks.