# Your First Workflow

Build a simple real workflow and learn the step pattern used throughout JsWorkflows.

This guide builds a simple workflow from scratch so you can learn the basic JsWorkflows pattern before moving on to more advanced templates and operational workflows.

If you prefer, you can also start from a template instead of writing your first workflow yourself. This page focuses on the from-scratch path so you can understand how workflow steps are structured.

## Watch the walkthrough

This video follows the same example workflow used in this guide: creating an `orders/paid` workflow, scheduling a follow-up step, testing the workflow, and checking the run output.

<div style={{ position: 'relative', paddingBottom: '56.25%', height: 0, overflow: 'hidden', borderRadius: '12px', border: '1px solid var(--sl-color-gray-5)' }}>
  <iframe
    src="https://www.youtube-nocookie.com/embed/9x0Pf-52Zag"
    title="Create your first JsWorkflows workflow"
    loading="lazy"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    allowFullScreen
    style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 0 }}
  />
</div>

## What we're building

A workflow that:

1. receives an `orders/paid` webhook
2. schedules a follow-up step for 5 minutes later
3. sends a Slack notification with the order details

This is a simple example, but it teaches a pattern you will use in more advanced workflows too, including approvals, operational checks, delayed follow-up, and multi-step automations.

## Why split work into steps

For Shopify webhook workflows, JsWorkflows acknowledges the incoming webhook before your workflow code runs. Your `start` step is not responsible for sending the HTTP response back to Shopify.

Even so, it is usually best to keep `start` focused on orchestration:

- validate the incoming data
- prepare the payload for later steps
- hand the real work off to another step

This matters because:

- `start` is the reserved entry point for a run
- `start` cannot be scheduled again with `api.scheduleNextStep()`
- delayed or retryable work should happen in later named steps
- scheduled steps run later as separate workflow executions

That makes workflows easier to recover, retry, extend, and reason about when something goes wrong downstream.

## The workflow code

Create a new workflow in the editor with the `orders/paid` trigger, then replace the code with:

```js
export class Workflow {
  /**
   * Entry step.
   * Keep this focused on validation, payload preparation, and scheduling.
   */
  async start(data, _headers, api) {
    const orderId = data.id;
    const orderName = data.name;
    const total = data.total_price;
    const currency = data.currency;

    await api.scheduleNextStep({
      delay: '5 min',
      action: 'notifySlack',
      payload: { orderId, orderName, total, currency },
    });

    console.log(`Queued Slack notification for ${orderName}`);
  }

  /**
   * Follow-up step.
   * This is where delayed or retryable work should happen.
   */
  async notifySlack({ orderId, orderName, total, currency }, _headers, api) {
    const { token, error } = await api.getOAuthToken('my-slack');
    if (error || !token) {
      console.log('Could not get Slack token:', error || 'Missing Slack token');
      return;
    }

    const res = await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        channel: 'C0123456789',
        text: `*New paid order* ${orderName} — ${total} ${currency}`,
      }),
    });

    if (!res.ok) {
      console.log(`Slack HTTP ${res.status}:`, await res.text());
      return;
    }

    const json = await res.json();
    if (!json.ok) {
      console.log('Slack API error:', json.error || 'unknown_error');
      return;
    }

    console.log(`Notified Slack for order ${orderName}`);
  }
}
```

In every step, the third argument `api` gives your workflow access to JsWorkflows platform helpers such as scheduling, OAuth tokens, logging, and state.

In this example:

- `api.scheduleNextStep()` queues the later Slack step
- `api.getOAuthToken('my-slack')` loads the access token for a saved Slack OAuth connection

The string `'my-slack'` is the OAuth handle. You create it when you connect Slack in JsWorkflows, then reuse that handle in your workflow code.

To create that Slack connection:

- go to **Settings → OAuth2 Tokens**, or
- in the workflow editor open **More actions → Manage OAuth2 tokens**

Then connect Slack and give the connection a handle such as `my-slack`. Your workflow code uses that handle later with `api.getOAuthToken(...)`.

## How it works

**The `start` step**:

1. reads the fields needed from the webhook payload
2. schedules `notifySlack` for 5 minutes later
3. exits after the follow-up work has been queued

**The `notifySlack` step**:

1. gets the Slack OAuth token by handle
2. posts the message to Slack using the channel ID
3. logs the result

The scheduled step runs later as a separate execution. It is not the original `start` step waiting in memory for 5 minutes. The payload passed to `api.scheduleNextStep()` is what the later step receives when it starts.

This same pattern scales into more advanced workflows:

- delayed follow-up checks
- approval and review flows
- fan-out processing
- retryable external API operations

For Shopify webhook workflows, JsWorkflows already deduplicates duplicate webhook deliveries before your workflow code runs. You would typically use `api.dedupe()` yourself only when you need business-level deduplication for your own workflow logic.

## Test it step by step

1. In the workflow editor, open **More actions → Test data**. This shows the **Request body** and **Request headers**, both of which are editable.

   For Shopify webhook triggers, the request body is pre-filled with the topic's sample payload. You can also inspect the sample at any time through **More actions → Sample payload**.

2. For a faster first test, temporarily change `delay: '5 min'` in the code to `delay: 10`. That is the minimum allowed delay and lets you verify the full flow quickly before switching it back to 5 minutes.

3. Leave the sample payload as-is, or adjust a few fields such as the order name, total, or currency if you want to make the test output easier to recognize.

4. Click **More actions → Run test**.

   The workflow starts from `start` exactly as a live run would. The `notifySlack` step is then scheduled and will run after the configured delay.

5. Watch the **Live Output** panel below the editor. It shows step executions in real time, including status, duration, step name, retries, and payload size.

> Caution: Test runs behave like live runs. Credits are consumed, and `fetch()` calls hit real endpoints. Use a test Slack channel while learning and testing.

## Enable it

When you are ready, toggle the workflow **Active**.

From that point on, every `orders/paid` event that matches the workflow trigger can start a run.

## What to learn from this page

Your first workflow does not need to be complicated. The important thing is to learn the basic structure:

- `start` is the reserved entry step
- `start` is best used for validation, payload preparation, and scheduling
- later steps do the delayed, retryable, or external work

Once that pattern is clear, the rest of the platform becomes much easier to understand.