> ## Documentation Index
> Fetch the complete documentation index at: https://docs.member.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Content automations

> Run steps for a file instead of a member: when a file lands in a folder or gets a tag, add it to playlists, publish it, change its privacy, or call a webhook.

A **content automation** is an automation that runs once for each **file** instead of once for each member. It is how you stop sorting new uploads by hand. For example: a recording lands in your Weekly calls folder. If its title contains "Q\&A", it goes into the Q\&A Replays playlist. Otherwise it goes into Coaching Calls, and either way it then goes through the Publish to hub step.

```text theme={null}
WHEN a file lands in the "Weekly calls" folder (and is ready)
  → title contains "Q&A"?
      yes → add to the "Q&A Replays" playlist, at the top
      no  → add to the "Coaching Calls" playlist, at the end
  → publish to the hub
  → end
```

Content automations use the same builder, lifecycle (draft, publish, activate), versions and webhook endpoints as the member automations in the [Automations guide](https://docs.member.dev/guides/automations.md). Read that page first if you have not built an automation before. This page covers only what is different when the thing running through the flow is a file.

Some terms used below:

* A **file** is one item in your Library: a video, audio file, PDF, image or other document. See [Media](https://docs.member.dev/guides/media-workflow.md).
* A **folder** groups files for your own housekeeping. Folders belong to your team, not to a hub.
* A **content tag** is a label you put on files, such as `coaching`. It is separate from the contact tags that member automations use.
* A **playlist** is an ordered list of files that a hub can show to its members.
* A file is **ready** when it can be played or opened. For video and audio that means the upload has finished and, when your deployment transcodes media, transcoding has finished too. For PDFs, images and other documents it means the upload has finished.

> [!WARNING]
>
> Content automations are in alpha. Release A, described here, covers both triggers, the four content steps, if / else, runs by file, and the conversion of legacy playlist rules. [Release A limits](#release-a-limits) lists what is not there yet. Do not rely on a behaviour that this page does not state.

## Member and content automations

An automation is one kind or the other, and the trigger decides which. The two kinds never mix in one automation.

| | Member automation | Content automation |
| - | - | - |
| Runs for | One member (contact) | One file |
| `subject_type` | `contact` | `file` |
| Triggers | Member events and custom events | **A file lands in a folder**, **A file is tagged** |
| Steps | Send email, tags, sequences, wait, if / else, webhook, end | Add to or remove from a playlist, publish to hub, set privacy, wait, if / else, webhook, end |
| Run history | `GET .../automations/{id}/enrollments` | `GET .../automations/{id}/content-runs` |
| Re-entry | `never`, `after_exit` or `interval` | `never` only: a file runs once |

`subject_type` is a read-only attribute on every automation. You never send it. The backend sets it from the trigger, so a content trigger makes a `file` automation. Once an automation has a published version, you cannot switch it between member and content triggers. An update that tries it is a `422` with the pointer `/data/attributes/definition/triggers/0/event_type`. Publishing a draft whose triggers do not match the automation's kind is a `422` with the pointer `/data/attributes/definition/triggers`. A member step in a content automation, or a content step in a member automation, is also a `422`.

To list only content automations, filter the list:

```text theme={null}
GET /api/v1/teams/{team_id}/hubs/{hub_id}/automations?filter[subject_type]=file
```

Use `filter[subject_type]=contact` for member automations. Leave the filter out to get both.

Some member routes refuse a file automation. `POST .../automations/{id}/enrollments` returns `422` with "This automation runs for a file, not a member." You cannot enroll a file by hand. A custom event cannot start a file run either, because the file triggers are the reserved `media.file.` events (see [Triggers](#triggers)).

## Triggers

A content automation has exactly one trigger. There are two:

```json theme={null}
{ "event_type": "media.file.ready_in_folder", "folder_id": "<folder id>" }
{ "event_type": "media.file.tagged",          "content_tag_id": "<content tag id>" }
```

| Trigger | Admin name | Needs | Fires when |
| - | - | - | - |
| `media.file.ready_in_folder` | A file lands in a folder | `folder_id` | A file in that folder becomes ready, or a ready file is moved into that folder. |
| `media.file.tagged` | A file is tagged | `content_tag_id` | A ready file gets that tag, or a file that already carries it becomes ready. |

A few rules apply to both:

* **One tag per trigger, by id.** Use the tag's id, not its name. To react to several tags, make one automation per tag.
* **The folder's own files only.** `folder_id` must be a folder of your team that still exists. A file in a subfolder does not fire the parent folder's automation.
* **No `entry_filter`.** Content triggers do not accept one.
* **Fires on ready, not on arrival.** A file that is still uploading or processing does not run anything yet. A file that is tagged while it is still processing runs once, when it becomes ready.
* **A file that fails processing** never runs its steps. The automation records one run with the status `skipped` and the reason `processing failed`, so you can see it under [Runs by file](#runs-by-file). That run uses up the file's one run. If the file fails processing and you replace it, the automation does not run for it again.
* **Once per file.** An automation runs at most once for a given file. Moving the file out of the folder and back, removing and re-adding the tag, or replacing the file's media does not run it again.
* **Moving many files.** Moving 500 ready files into a watched folder starts 500 runs. Moving a whole folder does not count as files landing in it.
* **Several automations on one folder or tag.** All of them run, each once per file, in no defined order. If two of them set different privacy values, or one adds a file to a playlist while another removes it, the last one to run wins.
* **Hubs.** A content automation belongs to one hub, like any automation, but the folder or tag it watches belongs to the team. Two automations in two hubs that watch the same folder run independently.

The names `media.file.ready_in_folder`, `media.file.tagged` and `media.file.processing_failed` are reserved. The custom-events endpoint rejects any event name that starts with `media.file.` with a `422` and writes nothing, so nobody can fake a file event.

### Tag a file

The tag trigger needs files that carry content tags. These routes create and list tags and set the tags on a file:

```text theme={null}
GET    /api/v1/teams/{team_id}/content-tags?filter[kind]=file
POST   /api/v1/teams/{team_id}/content-tags
DELETE /api/v1/teams/{team_id}/content-tags/{tag_id}
GET    /api/v1/teams/{team_id}/files/{file_id}/content-tags
PUT    /api/v1/teams/{team_id}/files/{file_id}/content-tags
```

* **List.** `GET /content-tags` returns every live tag of the team, ordered by name. `filter[kind]` is `file` or `playlist`. Leave it out to get both. The list is not paged.
* **Create.** `POST /content-tags` takes a `kind` (`file` or `playlist`) and a `name` of 1 to 255 characters. It answers `201`. A tag name that makes the same slug as a live tag of the same kind is a `422` with the code `duplicate_content_tag`. The tag trigger needs a `file` tag.
* **Delete.** `DELETE /content-tags/{tag_id}` answers `204`. It removes the tag from every file and playlist that carries it.
* **Read a file's tags.** `GET /files/{file_id}/content-tags` returns the file's `file` tags.
* **Set a file's tags.** `PUT /files/{file_id}/content-tags` replaces the whole tag set of the file. Send every tag the file should have afterwards, including the ones it already has. An empty `data` array clears them. Every id must be a live `file` tag of your team, or the call is a `422` with the pointer `/data`.

The body of `POST /content-tags`:

```json theme={null}
{
  "data": {
    "type": "content_tags",
    "attributes": { "kind": "file", "name": "coaching" }
  }
}
```

The body of `PUT /files/{file_id}/content-tags`:

```json theme={null}
{
  "data": [
    { "type": "content_tags", "id": "<content tag id>" }
  ]
}
```

A tag that the `PUT` adds to a file that is ready emits `media.file.tagged` once, and that can fire the trigger. A tag that the file already had emits nothing, and neither does removing a tag. If the file is not ready yet, nothing is emitted when you tag it. The file runs once, when it becomes ready.

In the admin, open a file in the Library. Its sheet has a **Content tags** field. Search for a tag and select it to add it, or type a new name and select **Create "name"** to make the tag and add it. Select a tag again, or the cross on its badge, to take it off. Each change saves at once. To tag many files in one go, select them in the Library and use **Add content tag** in the selection bar. It adds one tag to every selected file and keeps the tags they already have.

## Steps

Each step does one thing to the file that is running through the flow.

| Step | `type` | `config` | What it does |
| - | - | - | - |
| Add to playlist | `add_to_playlist` | `playlist_id`, `position` (`end` or `top`, default `end`) | Puts the file in the playlist. |
| Remove from playlist | `remove_from_playlist` | `playlist_id` | Takes the file out of the playlist. |
| Publish to hub | `publish_to_hub` | none (`{}`) | Publishes the file to the automation's own hub, visible to members. |
| Set privacy | `set_privacy` | `visibility` (`members`, `public` or `private`) | Sets who can see the file in the automation's hub. |
| If / else | `branch` | `conditions` (see [Branch on a file](#branch-on-a-file)) | Sends the run down a `yes` or `no` edge. |
| Wait | `wait` | `duration`, for example `"2 days"` | Pauses the run, as in member automations. |
| Call webhook | `webhook` | `webhook_endpoint_id` | Posts the [content payload](#the-webhook-payload) to a registered endpoint. |
| End | `exit` | optional `reason`, a short label (default `explicit_exit`) | Ends the run as `done`. |

The first node in `nodes` is where every run starts. Edges from an `if / else` node need a `branch_label` of `yes` or `no`, as in member automations.

**What each step checks and skips:**

* **Add to playlist** only adds video and audio. For a PDF, image or other document the step is `skipped` with the reason `playlist can't show this kind`. If the file stopped being ready (for example its media was replaced while the run waited), the step is `skipped` with `file not ready`. A file that is already in the playlist is `no_change`. A deleted playlist makes the step `failed` with `playlist deleted`, and the run stops. So does a playlist that was moved to another hub after you saved the automation: the step is `failed` with `playlist is in another hub`.
* **Playlist rules.** The playlist must belong to the automation's hub, or belong to no hub. Anything else is a `422` when you save.
* **Remove from playlist** on a file that is not in the playlist is `no_change`. A deleted playlist, or one in another hub, fails the step the same way as for **Add to playlist**.
* **Publish to hub** makes a new publication, live at once, with `members` visibility. If the file already has a publication in the hub, the step leaves it as it is and reports `no_change`, whether that publication is live, a draft or scheduled. A creator's draft or schedule is never overridden. A publication that an earlier playlist step created (a playlist mirror, see [Playlists and hubs](#playlists-and-hubs)) counts as an existing publication too: the step does not make it live, and the file keeps following its playlists. The `reason` of the `no_change` names the state, for example `already in the hub (draft)` or `already in the hub through a playlist (live)`.
* **Set privacy** changes the file's visibility in the automation's hub, which is the value that decides whether a member can open it. If the file has no publication in that hub yet, the step creates one as a draft with the visibility you chose, so you can keep a file private until someone reviews it and then publish it. It never changes the file's own visibility in the Library.
* **Files used as lessons.** `publish_to_hub` and `set_privacy` fail with `lesson file cannot be published` for a file that is attached to a lesson as a download.
* **A file deleted mid-run.** The run ends `skipped` with the reason `file deleted`.

### Playlists and hubs

Adding a file to a playlist also makes the file playable in every hub that shows that playlist. Where the file has no publication in such a hub, the step creates one that copies the playlist's own state there: the same visibility and the same publish date. This copy is called a **playlist mirror**.

A mirror follows its playlists. A file added to a draft, scheduled or private playlist stays hidden in the same way, and goes live when the playlist does. If the file is later added to a live playlist in the same hub, the mirror becomes live. When no playlist that serves the hub holds the file any more, the mirror is removed.

Only a mirror follows playlists. A publication that a playlist step did not create is never changed by one.

* **Publish to hub** leaves a mirror as it is. The step reports `no_change`, and the file keeps following its playlists. A file added to a draft playlist stays a draft until the playlist goes live. To make a file live whatever its playlists do, put **Publish to hub** before **Add to playlist**. The step then creates an ordinary live publication, and the playlist step leaves it alone.
* **Set privacy** takes a mirror over. It turns the mirror into an ordinary publication that stops following the playlist. It does this every time it runs on a mirror, even when you choose the visibility the mirror already has. It keeps the publish date.

In the example at the top of this page, **Publish to hub** comes after the playlist steps. If the playlist is live, the file is already live when **Publish to hub** runs. The step reports `no_change` with the reason `already in the hub through a playlist (live)`, and the file keeps following the playlist. If the playlist is a draft, the file stays a draft too. To publish the file whatever the playlist does, move **Publish to hub** to the start of the flow, before the **If / else** step.

Members never see a playlist item whose current media is still processing. Until the media is ready, the member playlist hides the item.

## Branch on a file

An **If / else** step (`branch`) in a content automation checks the file, not a member. It uses `conditions` instead of the `condition_tree` that member automations use.

```json theme={null}
{
  "id": "is_qa",
  "type": "branch",
  "config": {
    "conditions": {
      "match": "all",
      "checks": [
        { "check": "title_contains", "text": "Q&A" },
        { "check": "kind_is", "kind": "video" }
      ]
    }
  }
}
```

`match` is `all` (the default) or `any`. A branch has 1 to 20 checks.

| `check` | Extra field | The branch says yes when |
| - | - | - |
| `title_contains` | `text` | The file's title contains the text. Case does not matter. |
| `title_not_contains` | `text` | The title does not contain the text. |
| `folder_is` | `folder_id` | The file's own folder is that folder at the moment the branch runs. |
| `kind_is` | `kind`: `video`, `audio`, `document` or `image` | The file is of that kind. `document` covers PDFs and other documents. |
| `has_tag` | `content_tag_id` | The file carries that content tag at the moment the branch runs. |
| `not_has_tag` | `content_tag_id` | The file does not carry that tag. |

Member checks, such as a contact tag or a segment, are not available in a content automation.

## Build one with the API

The lifecycle is the same as for member automations: create a draft, publish it, then activate it. The create body carries the folder or tag in the trigger, and the steps in `nodes` and `edges`. This one builds the example from the top of the page:

```bash theme={null}
curl -s -X POST "$MIO_BASE_URL/api/v1/teams/$TEAM_ID/hubs/$HUB_ID/automations" \
  -H "Authorization: Bearer $MIO_ACCESS_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "automations",
      "attributes": {
        "name": "Sort weekly calls",
        "re_entry_mode": "never",
        "definition": {
          "triggers": [
            { "event_type": "media.file.ready_in_folder", "folder_id": "<weekly calls folder id>" }
          ],
          "nodes": [
            { "id": "is_qa", "type": "branch",
              "config": { "conditions": { "match": "all", "checks": [
                { "check": "title_contains", "text": "Q&A" }
              ] } } },
            { "id": "qa", "type": "add_to_playlist",
              "config": { "playlist_id": "<Q&A Replays playlist id>", "position": "top" } },
            { "id": "coaching", "type": "add_to_playlist",
              "config": { "playlist_id": "<Coaching Calls playlist id>", "position": "end" } },
            { "id": "publish", "type": "publish_to_hub", "config": {} },
            { "id": "done", "type": "exit", "config": {} }
          ],
          "edges": [
            { "from": "is_qa",    "to": "qa",       "branch_label": "yes" },
            { "from": "is_qa",    "to": "coaching", "branch_label": "no" },
            { "from": "qa",       "to": "publish" },
            { "from": "coaching", "to": "publish" },
            { "from": "publish",  "to": "done" }
          ]
        }
      }
    }
  }'
```

The `201` response has `"subject_type": "file"` in `data.attributes`. Then publish the draft and turn it on:

```bash theme={null}
curl -s -X POST "$MIO_BASE_URL/api/v1/teams/$TEAM_ID/hubs/$HUB_ID/automations/$AUTOMATION_ID/publish" \
  -H "Authorization: Bearer $MIO_ACCESS_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json"

curl -s -X POST "$MIO_BASE_URL/api/v1/teams/$TEAM_ID/hubs/$HUB_ID/automations/$AUTOMATION_ID/activate" \
  -H "Authorization: Bearer $MIO_ACCESS_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json"
```

Before anything is stored, the API checks the references and answers `422` with a `source.pointer` to the field when one is wrong: the folder or tag must exist and belong to your team, a tag must be a file tag, every playlist must exist in your team and be in this hub or in no hub, and a content automation cannot carry `re_entry_mode` other than `never`.

Where the ids come from:

| You need | Get it from |
| - | - |
| `folder_id` | `GET /api/v1/teams/{team_id}/folders`, see [Organize files with the API](https://docs.member.dev/guides/media-workflow.md#organize-files-with-the-api) |
| `playlist_id` | `GET /api/v1/teams/{team_id}/playlists` |
| `content_tag_id` | `GET /api/v1/teams/{team_id}/content-tags?filter[kind]=file` |
| `webhook_endpoint_id` | `GET /api/v1/teams/{team_id}/hubs/{hub_id}/webhook-endpoints`, see [Webhook endpoints](https://docs.member.dev/guides/automations.md#webhook-endpoints) |

### The webhook payload

A `webhook` step in a content automation sends a fixed body. You cannot add a `payload_template`, and the body never includes a member or contact field:

```json theme={null}
{
  "automation_id": "…",
  "run_id": "…",
  "node_id": "…",
  "trigger": "media.file.ready_in_folder",
  "file": {
    "id": "…",
    "title": "Weekly call",
    "kind": "video",
    "folder_id": "…",
    "folder_name": "Weekly calls"
  },
  "team_id": "…",
  "hub_id": "…"
}
```

The body that is delivered also carries `_meta.webhook_endpoint_id`. It is an internal field, so do not rely on it.

Delivery is signed and retried exactly like the member webhook step. See [Webhooks](https://docs.member.dev/guides/webhooks.md).

## Runs by file

A member automation lists its **enrollments**. A content automation lists its **runs by file**: one row for each file that ran through it, newest first.

```text theme={null}
GET /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{automation_id}/content-runs
```

```bash theme={null}
curl -s "$MIO_BASE_URL/api/v1/teams/$TEAM_ID/hubs/$HUB_ID/automations/$AUTOMATION_ID/content-runs?page%5Bsize%5D=20&filter%5Bstatus%5D=skipped" \
  -H "Authorization: Bearer $MIO_ACCESS_TOKEN" \
  -H "Accept: application/vnd.api+json"
```

The resource `type` is `automation_content_runs`. This response is shortened. It shows a run that ended `skipped` because the file was deleted partway through, which is what the `filter[status]=skipped` in the request asks for:

```json theme={null}
{
  "data": [
    {
      "type": "automation_content_runs",
      "id": "…",
      "attributes": {
        "automation_id": "…",
        "automation_version_id": "…",
        "file": {
          "id": "…",
          "title": "Weekly call",
          "kind": "video",
          "folder_id": "…",
          "deleted": true
        },
        "source": "upload",
        "trigger": "media.file.ready_in_folder",
        "status": "skipped",
        "reason": "file deleted",
        "started_at": "2026-10-21T16:00:00Z",
        "finished_at": "2026-10-21T16:00:01Z",
        "steps": [
          { "node_id": "qa", "type": "add_to_playlist", "outcome": "done",
            "reason": "added to the playlist", "at": "2026-10-21T16:00:01Z" },
          { "node_id": "publish", "type": "publish_to_hub", "outcome": "skipped",
            "reason": "file deleted", "at": "2026-10-21T16:00:01Z" }
        ]
      }
    }
  ],
  "meta": { "has_more": false, "count": 1 },
  "links": { "self": "…", "next": null }
}
```

| Field | Meaning |
| - | - |
| `file` | The file that ran: `id`, `title`, `kind` (`video`, `audio`, `document` or `image`, or `null` when the file has no media kind or one that the automation does not recognise), `folder_id` (`null` when the file is not in a folder), and `deleted` (`true` if the file has since been deleted). |
| `source` | How the file got here: `upload`, `zoom`, `google_drive`, `podcast`, `vimeo`, `dropbox`, `moved` (moved into the folder) or `tagged`. Files brought in from YouTube or a URL show as `upload`. |
| `trigger` | `media.file.ready_in_folder` or `media.file.tagged`. |
| `status` | `running` (in progress or waiting), then `done`, `skipped` or `failed`. A run that is stuck shows as `failed`. See [Stuck runs](#stuck-runs). |
| `reason` | Why a run is `skipped` or `failed`. A skipped or failed run always has text here. When nothing more specific was recorded it reads `This run was skipped.` or `This run failed.` It is `null` on a `running` or `done` run. |
| `started_at`, `finished_at` | When the run began and ended. `finished_at` is `null` while it runs. |
| `steps` | One entry for each step that ran: `node_id`, `type`, `outcome`, `reason` and `at`. |

A step `outcome` is `done`, `no_change` (nothing needed doing, such as a file already in the playlist), `skipped` or `failed`. A step `reason` is a short note in plain English. It can be `null`. A `failed` step always has one.

**Paging.** Use `page[size]` (1 to 100, default 20) and `page[after]`. Follow `links.next` for the next page; it is `null` on the last page. `meta.has_more` and `meta.count` are also returned. A malformed `page[after]` value is a `422` with the code `invalid_cursor`. Use the cursor from `links.next` as it is.

**Filtering.** `filter[status]` takes one of `running`, `done`, `skipped` or `failed`. Any other value is a `422`.

**Errors.** Asking for the content runs of a member automation returns `404` with the code `not_a_content_automation`. Member automations keep using `/enrollments`. The content runs list never contains member data.

### Stuck runs

A step that keeps raising errors is retried. When the retries run out, the run is stuck. The runs list shows a stuck run as `failed`, and its `reason` says that the retries ran out. `filter[status]=stuck` is not accepted here, so use `filter[status]=failed`.

Retry or skip a stuck run with the recovery routes of the [Automations guide](https://docs.member.dev/guides/automations.md#recovering-stuck-runs). Use the run's `id` as the `{enrollment_id}`:

```text theme={null}
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{automation_id}/enrollments/{run_id}/retry
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{automation_id}/enrollments/{run_id}/skip-step
```

Retry needs an `active` automation. Skip step refuses an `if / else` step. Both answer `409` with the code `enrollment_not_stuck` for a run that is not stuck. A run that failed on a content step, such as `playlist deleted`, has ended and cannot be retried. The admin's **Runs** tab does not offer these two actions.

## In the admin

The admin builds on the same Automations page you use for member automations, in the hub's **Automations** area.

**The list.** The automations list has a **Runs for** column. It says **Members** for a member automation and **Files** for a content automation.

**Build one.** Select **New automation** and give it a name. Select the trigger card, open the **Event** list, and pick one of the two entries under **Content**:

* **A file lands in a folder**, then choose a **Folder**.
* **A file is tagged**, then choose a **Content tag**.

The trigger card then reads "A file lands in *folder*" or "A file is tagged *tag*", with "When ready · once per file" underneath, and a note says "A file runs through this automation once." The member filter ("Only for people who…") and the re-entry choices are hidden, because they do not apply to files.

Use the **Add a step** button between the cards. With a file trigger the menu offers **Add to playlist**, **Remove from playlist**, **Publish to hub**, **Set privacy**, **If / else** (hint: "rules on the file"), **Wait**, **Call webhook** and **End**. Select a step card to fill it in:

* **Add to playlist** and **Remove from playlist** ask for a **Playlist**. **Add to playlist** also asks for a **Position**, **At the end** or **At the top**, and notes "Files this playlist can't show are skipped."
* **Set privacy** asks for a **Visibility**: **Members**, **Public** or **Private**.
* **Call webhook** asks for an **Endpoint**. Endpoints are registered under **Account › Developer tools › Webhooks**, see [Developer tools](https://docs.member.dev/guides/platform-account.md#developer-tools). The **Extra fields to send** box that member automations have is hidden here, because a file webhook sends a fixed payload.
* **If / else** asks you to choose **Match**, **All checks** or **Any check**, then lets you add up to 20 checks with **Add check**: **Title contains**, **Title does not contain**, **Folder is**, **File kind is**, **Has tag** and **Does not have tag**.

Select **Save draft**, publish a version from the **Versions** tab, then select **Activate**.

Once an automation is active, paused or archived, its steps are read-only in the builder, and a published version cannot be reopened. To change an active or paused one, use **Duplicate** in the row menu of the automations list, which makes a new draft copy. An archived automation has no **Duplicate**. **Pause** stops new runs from starting. The admin confirms it with "No new runs start it. Runs already started carry on where they are."

**Runs.** On a content automation the third tab is **Runs** instead of **Enrollments**. It has one row for each file run, in a table with the columns **File**, **Source**, **Started**, **Status**, **Reason** and **Steps**. Above the table, a filter has **All**, **Running**, **Done**, **Skipped** and **Failed**.

* **Source** reads **Upload**, **Zoom**, **Google Drive**, **Podcast**, **Moved into folder** or **Tagged**. A file that has since been deleted is marked **Deleted**.
* **Reason** shows the run's reason for a skipped or failed run. Any other run shows a dash.
* **Steps** opens a row to show each step, its outcome (**Done**, **No change**, **Skipped** or **Failed**) and its reason when it has one. A run with no finished step yet says "No steps yet."
* **Load more** under the table shows the next page of runs.
* With no runs, the table says "No runs yet. Runs start once a file matches an active automation." When a filter matches none, it says "No runs match this filter."
* If the first load fails, the tab says "Couldn't load runs" with a **Retry** button. If a refresh fails after rows are on screen, a notice above the rows says "Couldn't refresh this list. What you see may be out of date." with a **Retry** button, and the rows stay.

## Release A limits

These limits are on purpose. They are the first release, not the final behaviour.

* **No "Test with a file" yet.** The admin shows the **Test with a file** button on a content automation, but it is turned off, and the builder says "Testing with a file isn't available yet." The **Test with a file** item in the row menu of the automations list is turned off too, with the same note. `POST .../automations/{id}/test` on a content automation returns `422` with "Test with a file is not available yet" before anything runs. Testing a member automation works as before. To check a content automation, activate it on a folder or tag you control and read its runs.
* **No backfill of ready files.** Turning an automation on does not run it for files that are already **ready** and in the folder, or already ready and carrying the tag. A file that is already in the folder (or already tagged) but is still processing when you activate does run once it becomes ready. From then on, a file runs when it becomes ready, is moved into the folder, or gets the tag. There is no backfill yet.
* **Nothing is undone.** If a file is moved out of the folder, or the tag is removed, the file stays in the playlists the automation put it in.
* **One run per file.** A content automation accepts only `re_entry_mode: "never"`. Any other value is a `422`.
* **Manual imports land at the top level of the Library.** An import you start yourself from Zoom, Google Drive, Dropbox, Vimeo or a podcast is not put in a folder. The automatic imports of a Google Drive, Dropbox or Vimeo subscription land in the library folder the subscription is set to import into, or at the top level when it has none. Zoom and podcast imports have no landing folder yet. A folder automation therefore starts for a file that was imported to the top level only after you move it into the folder. Tag automations are not affected.

### Legacy playlist rules

If you are moving from the legacy Searchie app, your playlist rules are converted into content automations as part of the migration, so files keep flowing into the same playlists:

* A folder rule becomes **A file lands in a folder**, then **Add to playlist** at the end.
* A tag rule becomes **A file is tagged**, then **Add to playlist** at the end.
* A playlist that was fed by both a folder and a tag becomes two automations, because in the legacy app either rule alone added the file.

Each converted automation is named after its playlist and its source, for example "Coaching Calls · from Weekly calls". The conversion does not run the new automations on files that are already in a playlist, so nothing is added twice.

## Related

* [Automations](https://docs.member.dev/guides/automations.md) covers the lifecycle, versions, run counts and webhook endpoints that content automations share.
* [Media](https://docs.member.dev/guides/media-workflow.md) covers files, folders, playlists and visibility.
* [Webhooks](https://docs.member.dev/guides/webhooks.md) covers signed delivery.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.