# Overview

The api object and global env available in every workflow step.

Every executable step method, including `start` and any named continuation step you schedule later, receives three arguments:

```js
async start(data, headers, api) { ... }
```

| Argument | Description |
| --- | --- |
| `data` | The trigger payload or step payload (order object, HTTP body, scheduled context, or the `payload` from `api.scheduleNextStep()`) |
| `headers` | Trigger/request headers when the step was entered from an HTTP-style source. For scheduled runs and most continuation steps, treat this as optional context and do not rely on it unless the trigger docs say it is present. |
| `api` | The JsWorkflows API object, which exposes platform capabilities |

## api methods

| Method | Description |
| --- | --- |
| [`api.scheduleNextStep()`](/workflow-api/scheduling/) | Schedule the next step (or fan-out to multiple steps) |
| [`api.waitForEvent()`](/workflow-api/wait-for-event/) | Pause a run and resume it when an external event arrives |
| [`api.dedupe()`](/workflow-api/scheduling/#deduplication--apidedupe) | Prevent duplicate processing within a time window for your own business logic or external-event idempotency |
| [`api.getOAuthToken()`](/oauth/overview/) | Get a valid access token for a connected OAuth service |
| [`api.google.getServiceAccountToken()`](/workflow-api/google-service-account/) | Get a Google access token using a service account key (cached 58 min) |
| [`api.google.sheet.*`](/workflow-api/google-service-account/#apigooglesheet) | Google Sheets helpers such as `appendRows`, `readRows`, `updateRows`, and `clearRange` |
| [`api.csv.*`](/workflow-api/csv/) | Import a CSV from a URL, split it into chunks, and process each chunk in a separate step |
| [`api.runStore.*`](/workflow-api/state/) | Store and retrieve values scoped to the current run |
| [`console.log()`](/workflow-api/logging/) | Write debug output visible in the test runner and run history |
| [`api.getAttachment()`](/workflow-api/attachments/) | Retrieve an email attachment as an `ArrayBuffer` (email-trigger workflows only) |
| [`api.deleteAttachment()`](/workflow-api/attachments/) | Delete an email attachment from storage (email-trigger workflows only) |
| [`api.sendToFlow()`](/triggers/shopify-flow/#pushing-data-into-flow-from-any-step) | Fire a JsWorkflow Data Received trigger in Shopify Flow from any step |
| [`api.sendEmail()`](/workflow-api/send-email/) | Send a notification email to your configured notification address (paid plans) |

## Global env object

All secret variables you store via **More actions → Manage variables** in the workflow editor are available as properties on the global `env` object:

```js
export class Workflow {
  async start(data, headers, api) {
    const apiKey = env.MY_API_KEY;   // a secret you stored
    const shop   = env.SHOPIFY_STORE; // auto-injected: "mystore.myshopify.com"
    const ver    = env.SHOPIFY_API_VERSION; // auto-injected: e.g. "2026-04"
  }
}
```

Two values are always present regardless of your stored secrets:

| Key | Value |
| --- | --- |
| `env.SHOPIFY_STORE` | Your store's myshopify.com domain |
| `env.SHOPIFY_API_VERSION` | The Shopify API version configured for your app |

For Shopify webhook triggers, duplicate delivery protection already happens before your workflow code runs. `api.dedupe()` is still useful when you need your own workflow-specific or business-level deduplication rules.

## Making HTTP requests

Use the global `fetch()` to call any URL. For Shopify Admin API requests, the platform automatically injects the `X-Shopify-Access-Token` header when the URL matches your store domain, so no extra configuration is needed:

```js
// Shopify GraphQL, token injected automatically
const res = await fetch(
  `https://${env.SHOPIFY_STORE}/admin/api/${env.SHOPIFY_API_VERSION}/graphql.json`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: `query { orders(first: 10) { nodes { id name } } }` }),
  }
);
const { data } = await res.json();
```

For OAuth-connected services, retrieve the token with `api.getOAuthToken()` and add it yourself:

```js
const { token } = await api.getOAuthToken('my-slack');
const res = await fetch('https://slack.com/api/chat.postMessage', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
  body: JSON.stringify({ channel: '#orders', text: 'Hello' }),
});
```

## Lifecycle hooks

Your `Workflow` class can optionally define two lifecycle hooks that run once when the entire run reaches a terminal state. These hooks do **not** use the normal `(data, headers, api)` step signature:

```js
export class Workflow {
  async start(data, headers, api) { ... }

  // Called when the entire run (all branches) completes successfully
  async onWorkflowComplete(api) { ... }

  // Called when a step throws an uncaught error that terminates the run
  async onWorkflowError(err, api) { ... }
}
```

Inside hooks, `api` is restricted to: `getOAuthToken`, `google`, `log`, `dedupe`, `runStore`, and `sendEmail`. The methods `scheduleNextStep`, `waitForEvent`, and `csv` are not available. The global `fetch()` API is still available inside hooks.

See [Lifecycle Hooks](/workflow-api/lifecycle-hooks/) for full behavioral rules and examples.