# Workflows

> Source: https://truto.one/docs/guides/workflows/overview/

A **workflow** starts a sync job for you when something happens in your environment, such as an account finishing its connection. Without one, your app has to catch the `integrated_account:active` webhook and call `POST /sync-job-run` itself.

:::skip-reading{cli="truto workflows list -o json" skill="truto-jsonata"}
:::

Workflows have no screen in the dashboard. Create and manage them with the API, the CLI or an SDK.

## How a workflow runs

A workflow has a `trigger_name` and a `config`:

- When the trigger fires in the workflow's environment, Truto evaluates the workflow's `run_if`. If it's falsy, nothing else happens.
- Otherwise Truto creates a **workflow run** and runs the steps in order, one at a time. A step with its own `run_if` that evaluates falsy is skipped.
- The run ends `completed`, or `failed` if any step failed. Its `result` lists each step with `success`, and `error` or `result`.

Every workflow in the environment with the same trigger runs, each with its own run.

## Triggers

| `trigger_name` | Fires when |
|---|---|
| `integrated_account:active` | An account finishes connecting through a link or the connect flow: its post-install and validation actions passed and it has no post-connect form left to fill in. It fires again when an existing account reconnects. |
| `unified_webhook:batch` | Truto processed a batch of inbound vendor webhook events for an account into `record:*` events. See [Receiving vendor webhooks](/docs/guides/webhooks/inbound-vendor-webhooks). |

The workflow's `run_if` and its steps see `integrated_account_id`, `environment_id`, `integration` (the account's integration, with `integration.name`), `context` (the account's context) and `tenant_id`. Steps also see `payload`, the trigger's data: for `unified_webhook:batch` that's `integrated_account_id`, `events` and `batch_size`.

Creating a workflow with any other `trigger_name` returns `404`.

## Steps

`config.steps` is an array. Each step has `type: "run"` and `action: "run_sync_job"`, which is the one action there is.

| Field | What it does |
|---|---|
| `config` | The body of the sync job run to create, as an object with `{{placeholders}}` or a JSONata string that returns it. It must include `sync_job_id`. The rest is what `POST /sync-job-run` takes: `integrated_account_id` and a `webhook_id` or `datastore_id` for runtime versions 1 to 3, or `args` for [version 4](/docs/guides/sync-jobs/v4-reference). |
| `cron_expression` | Instead of running the sync job once, create a schedule for it with this cron expression. The step's `config` becomes the schedule's body. |
| `run_if` | A JSONata expression. The step runs only when it's truthy. |

A run created by a step sends `sync_job_run:created` to your webhooks, like any other run.

## Run a sync job when an account connects

This workflow runs a V4 sync job for every HubSpot or Salesforce account that connects, and sends the output to one webhook.

:::code-tabs

```bash label="cURL"
curl -X POST "https://api.truto.one/workflow" \
  -H "Authorization: Bearer <API token>" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_name": "integrated_account:active",
    "config": {
      "run_if": "integration.name in [\"hubspot\", \"salesforce\"]",
      "steps": [
        {
          "type": "run",
          "action": "run_sync_job",
          "config": "{ \"sync_job_id\": \"<sync_job_id>\", \"args\": { \"integrated_account_id\": integrated_account_id, \"webhook_id\": \"<webhook_id>\" } }"
        }
      ]
    }
  }'
```

```bash label="CLI"
truto workflows create -b '{
  "trigger_name": "integrated_account:active",
  "config": {
    "run_if": "integration.name in [\"hubspot\", \"salesforce\"]",
    "steps": [
      {
        "type": "run",
        "action": "run_sync_job",
        "config": "{ \"sync_job_id\": \"<sync_job_id>\", \"args\": { \"integrated_account_id\": integrated_account_id, \"webhook_id\": \"<webhook_id>\" } }"
      }
    ]
  }
}'
```

:::

The step's `config` is a JSONata string, so `integrated_account_id` is read from the trigger. To pick a different sync job per integration, map on `integration.name`:

```text
(
  $sync_job_id := $lookup({
    "hubspot": "<hubspot_sync_job_id>",
    "salesforce": "<salesforce_sync_job_id>"
  }, integration.name);
  {
    "sync_job_id": $sync_job_id,
    "integrated_account_id": integrated_account_id,
    "webhook_id": "<webhook_id>"
  }
)
```

To keep the account in sync after the first run, add `"cron_expression": "0 */6 * * *"` to the step. The workflow then creates a schedule for the account instead of a single run. Schedules run in UTC, at most every five minutes.

## Check what a workflow did

`GET /workflow-run?workflow_id=<id>` lists a workflow's runs. Filter by `status`: `created`, `running`, `completed` or `failed`. A run's `result.steps` says which steps ran, which were skipped, and why a step failed.

:::code-tabs

```bash label="cURL"
curl "https://api.truto.one/workflow-run?workflow_id=<workflow_id>&status=failed" \
  -H "Authorization: Bearer <API token>"
```

```bash label="CLI"
truto workflow-runs list --workflow_id <workflow_id> --status failed
```

:::

If a run fails, Truto retries it, and a retry runs every step again. A step that already created a sync job run can create another, so keep steps that can't run twice out of workflows whose other steps may fail. You can't start a workflow run yourself: only its trigger does.

## Next

The full set of endpoints is in the [Workflows](/docs/api-reference/admin/workflows) and [Workflow runs](/docs/api-reference/admin/workflowruns) API references. For the sync job the step starts, see the [V4 sync job reference](/docs/guides/sync-jobs/v4-reference).
