Functions in Swell are powered by Cloudflare Workers and executed at the edge for optimal performance. Functions have no cold boots and do not require extra time to start up before executing your code. Since they run on the edge, they are optimal both for server-side webhooks and for serving personalized content directly to end-users.

You can create a new function by adding a JavaScript or TypeScript file in the app's functions/ folder, or by using the following CLI command:

swell create function

→ See the CLI reference for more details and options.

Functions run with the Cloudflare Worker runtime, a high-performance environment designed for efficient and secure server-side execution. It offers a subset of Web APIs, including fetch, Request, and Response, optimized for minimal latency and quick execution. The runtime is not a complete Node.js environment and does not support certain Node.js APIs, such as reading or writing to the filesystem.

The platform can trigger functions in response to various platform events, providing a powerful way to implement App-specific logic within Swell. When an event occurs, the corresponding app webhook is triggered, executing the associated Function. This allows developers to handle a wide range of use cases, such as sending custom notifications, processing data, or integrating with third-party services.

Due to their nature, functions don't have access to all Node.js and Browser APIs. Developers need to use ES modules and avoid libraries that rely on dynamic code execution or unsupported Node.js APIs. Function restrictions include limits on initial response time, memory usage, code size, and environment variable sizes. Additionally, request properties such as URL length, request body length, and request headers are subject to constraints. As the Workers runtime evolves, it is expected that more Node.js APIs and functionality will become available, further enhancing the capabilities of app functions.

Functions must respond within 10 seconds by default. The timeout configuration accepts values from 1,000 to 20,000 milliseconds, and values above 10,000 require the functions.long_timeout feature to be enabled for the store. For work that outlasts the response window, respond immediately and continue processing in the background with req.context.waitUntil(), or split large jobs into smaller batches triggered by cron schedules or workflows.

Pricing for functions is based on the number of executions. There is no limit on the total number of function executions an app can make, but charges apply for executions above the store’s plan tier.

During local development, you'll be able to run app functions on your machine and quickly test changes using the following CLI command:

swell app dev

This will perform several functions:

  • Start a local proxy using localtunnel.me.
  • Add the local proxy URL to your account session, which informs Swell that your app dev server is running and should receive events instead of a deployed version.
  • Note: This command will also start a development server for an app frontend, if one exists.

You should only run app dev on one app at a time, otherwise your account session will only remember the last app that was started.

Events triggered by your Swell store will be routed to your local server, allowing you to observe and iterate on app logic.

There are a variety of triggers that can be utilized with functions that support a wide variety of applications.

Functions can be triggered by events from your data models. To connect a function to a model event, specify the model.events property with one or more event names. In the following example, we’re using the conditions property to narrow function invocations where the event stock_level property is being changed. Note the function is called asynchronously.

functions/product-updates.ts
export const config: SwellConfig = {
  description: 'Update product records when stock levels change',
  model: {
    events: ['product.created', 'product.updated', 'product.deleted'],
    // conditions are checked against the record before calling the function
    conditions: {
      stock_level: { $exists: true },
    },
    // optional sequence number to prioritize among other function calls
    // a lower number means higher priority
    sequence: 1
  },
};

export default function (req: SwellRequest) {
  const { swell, data, session, store } = req;
  ...
}

For a full list of standard model events, see the event types reference.

You can run scheduled tasks at a later date in response to record data. Swell uses this capability natively to schedule invoices, payments, cart abandonment, and many other platform tasks. Use the model.schedule property to indicate the future date for a scheduled task.

functions/scheduled-payment.ts
export const config: SwellConfig = {
  description: 'Capture a custom scheduled payment',
  model: {
    events: ['payment.created', 'payment.updated'],
    conditions: {
      // in this example, 'date_capture_scheduled' is an App-defined field
      date_capture_scheduled: { $exists: true },
    },
    schedule: {
      // formula evaluated in context of the record
      // this will cause your function to be called on the date in the field
      // updates any time the field 'date_capture_scheduled' is set or changed
      formula: 'date_capture_scheduled',
    }
  },
};

export default function (req: SwellRequest) {
  const { swell, data, session, store } = req;
  ...
}

Instead of using a record field to indicate the date to execute your function, you can use a simple Cron schedule. Functions executed by a Cron schedule do not have any particular record context.

functions/updare-product-popularity-daily.ts
export const config: SwellConfig = {
  description: 'Update product popularity on a daily basis',
  cron: {
    schedule: '0 0 * * *' // call this function every day at midnight
  }
};

export default function (req: SwellRequest) {
  const { swell, data, session, store } = req;
  ...
}

Functions can also be exposed as custom API endpoints by exporting one or more route handler methods, get, put, post, or delete. These functions are deployed to an edge network for optimal end-user performance.

By default, you must pass an API secret key to invoke the function from an external server, or set the route.public property to indicate the function should be publicly accessible without a key.

functions/analyze-subscriptions.ts
export const config: SwellConfig = {
  description: 'Retrieve subscription analytics',
  route: {
    methods: ['get'], // any of get, put, post, or delete
    headers: { ... }, // specify static headers, such as an authentication secret
    cache: {
      timeout: 5000, // cache the output for 5 seconds
    },
    public: false // true to allow requests without a secret key
  }
};

export async function get (req: SwellRequest) {
  const { swell } = req;

  return await swell.get('/subscriptions', {
    $aggregate: [
      // ... aggregation pipeline
    ],
  });
}

In this example, a GET request to https://<secret_key>@<store_id>.swell.store/functions/<app-id>/analyze-subscriptions will trigger the analyze-subscriptions.js function and return the result.

Route functions are also served through the store gateway at https://<store-id>.swell.store/functions/<app-id>/<function-name>. External requests to this URL must include an app API key in the Authorization header. The key identifies both the app and the target environment — a test key routes the request to the Test environment, and a live key to the Live environment. Without a valid key, the gateway cannot resolve the app and responds with a 404 error.

Note that query string parameters sent through the gateway arrive in req.data when the request has no body. To handle both direct and gateway invocations consistently, read parameters from both req.query and req.data.

A function must only specify one of the following trigger types: model, route, or cron, but not more than one.

The following table outlines functions available for use within Swell App development.

AttributeDescription
descriptionA brief description of the function.
modelConfigure this function to be called by model events.
model.eventsAn array of model events, for example [”product.updated”].
model.conditionsOptional object with operators to filter function invocation based on specific properties in a request.
model.sequenceOptional number to indicate priority among other App triggers for the same model events. A lower number is considered higher priority.
model.scheduleOptional schedule.
model.schedule.formulaA specific field or formula to derive the scheduled task date from.
routeExternal route configuration.
route.publicOptionally indicate this function should be accessible without an API key.
route.methodsOptional array of methods to allow the function to be called by.
route.cacheConfigure the cache behavior for get requests to the function.
route.cache.timeoutNumber of milliseconds to cache the result of a get request. Defaults to 5000.
cronConfigure this function to be called by a Cron schedule.
cron.scheduleA string describing a Cron schedule to invoke this function independently of an model event or external request.

Function architecture is meant to simplify your app code by deeply integrating with Swell and leveraging the Cloudflare Worker platform to the greatest extent possible.

Workflows are durable, multi-step background functions that persist across restarts and support long-duration pauses. A workflow breaks its logic into retriable steps using step.do() and can pause with step.sleep() without consuming execution time. Use them for multi-step background work that needs to survive failure between steps — syncing to an external system, extended fulfillment operations, scheduled follow-ups, imports, and batch jobs that exceed normal function timeouts.

Workflows are currently in beta, gated per store. Contact Swell to enable them for your store.

A workflow is a function file whose config sets kind: 'workflow'. The only other key a workflow config accepts is description — workflows can't declare route, model, cron, extension, or timeout, so they can't be triggered by a route, a model event, or a cron schedule. They're started explicitly from another function.

The config must be a static object literal — spreads, computed keys, and imported constants are rejected. The file must have exactly one default export, and it must be a class declared in the same file with an instance method named run. The class is constructed with no arguments.

functions/sync-order.ts
export const config = {
  kind: 'workflow',
  description: 'Sync an order to the warehouse',
};

export default class SyncOrder {
  async run(req: SwellWorkflowRequest, step: SwellWorkflowStep) {
    const order = await step.do('load order', async () => {
      return req.swell.get(`/orders/${req.data.order_id}`);
    });

    await step.sleep('cool off', '30 seconds');

    await step.do('mark synced', async () => {
      return req.swell.put(`/orders/${order.id}`, {
        $app: { [req.appId]: { synced: true } },
      });
    });

    return { ok: true };
  }
}

Each step.do(name, fn) executes once and its result is durably recorded, so a step that already completed isn't re-run when the workflow retries. Keep side effects inside steps and return values that survive serialization; anything you do between steps runs again on every retry.

Use step.sleep(name, duration) to pause for a relative duration, either a string like '30 seconds' or a number of milliseconds, and step.sleepUntil(name, date) to pause until a specific time, given as a Date, an ISO string, or a timestamp. A sleeping workflow isn't running, so long pauses don't consume execution time.

An optional second argument tunes retry behavior for a single step. Within retries, limit and delay are both required, and backoff accepts 'constant', 'linear', or 'exponential'. When an API call inside a step fails with a non-retryable error, the step stops retrying immediately rather than exhausting its limit.

Tune retries for a step
await step.do(
  'call warehouse',
  {
    retries: { limit: 3, delay: '10 seconds', backoff: 'exponential' },
    timeout: '30 seconds',
  },
  async () => {
    // ...
  },
);

Start a workflow from any ordinary app function with req.swell.workflows.create(), passing the workflow's name and its parameters. It returns the new instance's id (prefixed wf_inst_) and a status of active. The call returns as soon as the instance is created — it doesn't wait for the workflow to finish. The workflow must already be deployed, or the call fails with workflow_not_deployed.

Parameters must be JSON-safe and no larger than 128 KB once serialized. Only null, finite numbers, strings, booleans, plain arrays, and plain objects are accepted — a Date, a class instance, or undefined is rejected rather than coerced, with the code workflow_params_unserializable; oversized params fail with workflow_params_too_large. Pass identifiers rather than records, and re-fetch the data you need inside the workflow.

Start a workflow from a function
const run = await req.swell.workflows.create('sync-order', {
  order_id: id,
});

// { id: 'wf_inst_...', status: 'active' }

A workflow request is narrower than an ordinary function request. You get req.data (the parameters it was started with), req.swell.get/post/put/delete, and req.swell.settings(), which takes no argument and always returns your own app's settings. Instance metadata lives on req.workflow as workflow_id, workflow_name, workflow_instance_id, trigger, and request_id — the last being the id of the function request that started it, which is how workflow logs correlate back to the caller.

Some things available elsewhere are not available here. There's no req.appValues() helper — write app-namespaced fields explicitly as { $app: { [req.appId]: { ... } } }, as in the example above. A workflow also can't start another workflow, and requests to meta endpoints are blocked: any path whose first segment begins with : is rejected with workflow_operation_blocked, with a single exception for /:batch, whose child operations are checked under the same rules.

Workflows are deploy-only. They don't run under swell app dev — your other functions still do, but a workflow only runs after swell app push. The iteration loop is to push, trigger the function that starts the workflow, then inspect the result. A function running locally can still call workflows.create(), as long as the workflow it names has already been pushed.

Deploy and inspect a workflow
# Scaffold a workflow function
swell create function sync-order --type workflow

# Validate the file before pushing
swell schema function functions/sync-order.ts

# Workflows only run once deployed
swell app push

# Deployed workflows, and their recent run summary
swell inspect workflows --app=.

# Recent instances, newest first (default limit 10)
swell inspect workflow-runs --app=. --status failed

# A single run in detail
swell inspect workflow-runs wf_inst_... --app=. --json

# Step-level logs, scoped to your app
swell logs --type workflow --app my-app

A run can end as completed, terminated, or failed, and a failed run records whether it failed while being created or while running. There's no retry command: to re-run a failed instance you start a new one with the same parameters, so it's worth making workflows idempotent on their params. There is no dedicated termination command either — swell inspect workflow-runs wf_inst_... prints a ready-to-run swell api post command for an active run. Note that run parameters are redacted from CLI output, so if you need to see what a run received, log it yourself inside a step.

  • An app can define at most 25 workflows.
  • Instance creation is rate limited to 60 per minute per store, environment, and app. Exceeding it returns workflow_create_rate_limited, which is safe to retry.
  • Parameters are capped at 128 KB of serialized JSON.
  • Workflow logs are retained for 30 days.

Limits on step retries, total workflow duration, and maximum sleep length come from the underlying durable execution runtime rather than from Swell.