> ## Documentation Index
> Fetch the complete documentation index at: https://developers.apps.filed.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Export a firm's protocols

> List the firm's own protocols, read their full content, export them to a CSV, and turn one on or off

Firm protocols are the rules the Filed analyst follows for everyone in a
workspace, shown under **Playbook** in the Filed web app.

> [!NOTE]
>
> The API calls protocols **skills**. A firm protocol is a `Skill` with
> `kind: WORKSPACE`; see the [Skills reference](https://developers.apps.filed.com/apis/skills.md).

```mermaid theme={null}
---
config:
  theme: base
  look: classic
  layout: elk
  fontFamily: "Geist, ui-sans-serif, system-ui, sans-serif"
  flowchart:
    curve: linear
  themeVariables:
    fontSize: 14px
    primaryColor: "#ffffff"
    primaryBorderColor: "#d4cabc"
    primaryTextColor: "#1e150e"
    lineColor: "#a89a88"
    edgeLabelBackground: "#ffffff"
---
flowchart LR
  A["1. List<br/>workspace.skills"] --> B["2. Read<br/>workspace.skill"]
  B --> C["3. Export<br/>protocols.csv"]
  A -.-> D["4. Turn on or off<br/>setSkillActive"]
  classDef out fill:#e9f6e9,stroke:#257035,color:#1e150e
  class C out
```

Every call uses a workspace token, from an [API key](https://developers.apps.filed.com/guides/authentication.md) or
an [OAuth grant](https://developers.apps.filed.com/guides/connect-with-oauth.md), and goes to
`https://router.apps.filed.com/graphql`.

## 1. List the firm protocols

```graphql Query theme={null}
query FirmProtocols {
  me {
    ... on WorkspaceUser {
      workspace {
        skills {
          kind
          taskType
          name
          description
          status
          mergedInto
          tags
          updatedAt
        }
      }
    }
  }
}
```

```bash cURL theme={null}
curl -X POST https://router.apps.filed.com/graphql \
  -H "Authorization: Bearer YOUR_WORKSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ me { ... on WorkspaceUser { workspace { skills { kind taskType name description status mergedInto tags updatedAt } } } } }"}'
```

Omit `taskType` to list every task family at once. The list also holds the
caller's personal protocols (`kind: USER`); keep the rows with
`kind: WORKSPACE`.

| `status` | Means |
| - | - |
| APPROVED | On: the analyst applies it |
| DISABLED | Off, still listed |
| PENDING | A user's personal protocol waiting for an admin to approve it for the firm |
| MERGED | Folded into the protocol named in `mergedInto`; it has no content of its own |

## 2. Read a protocol's content

The list leaves out `body`, the protocol's text. Read it one protocol at a time
with `skill`, passing the `kind`, `taskType` and `name` from the list row.

```graphql Query theme={null}
query FirmProtocol($kind: SkillKind!, $taskType: String!, $name: String!) {
  me {
    ... on WorkspaceUser {
      workspace {
        skill(kind: $kind, taskType: $taskType, name: $name) {
          name
          body
        }
      }
    }
  }
}
```

```json Variables theme={null}
{
  "kind": "WORKSPACE",
  "taskType": "tax-prep",
  "name": "consolidated-brokerage-1099s"
}
```

`body` is Markdown.

## 3. Export them all to a CSV

**1. Save the script**

It lists the firm protocols, reads their content and writes
`protocols.csv`. It needs Python 3.9 or later and nothing else. It reads
25 protocols per request, each under its own alias, and waits out a `429`
if the rate limit is hit.

```python export_protocols.py theme={null}
import csv
import json
import os
import time
import urllib.error
import urllib.request

URL = os.environ.get("FILED_GRAPHQL_URL", "https://router.apps.filed.com/graphql")
TOKEN = os.environ["FILED_TOKEN"]
BATCH = 25

LIST = """
query FirmProtocols {
  me { ... on WorkspaceUser { workspace {
    skills {
      kind taskType name description status mergedInto tags updatedAt
    }
  } } }
}"""


def workspace(query):
    request = urllib.request.Request(
        URL,
        data=json.dumps({"query": query}).encode(),
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
    )
    while True:
        try:
            with urllib.request.urlopen(request) as response:
                payload = json.load(response)
            break
        except urllib.error.HTTPError as error:
            if error.code != 429:
                raise
            time.sleep(int(error.headers.get("Retry-After", "60")))
    if payload.get("errors"):
        raise SystemExit(json.dumps(payload["errors"], indent=2))
    return payload["data"]["me"]["workspace"]


def read_bodies(protocols):
    """One request for a batch: alias p0, p1, ... per protocol."""
    fields = "\n".join(
        f"p{i}: skill(kind: {p['kind']}, taskType: {json.dumps(p['taskType'])}, "
        f"name: {json.dumps(p['name'])}) {{ body }}"
        for i, p in enumerate(protocols)
    )
    found = workspace("query { me { ... on WorkspaceUser { workspace {\n" + fields + "\n} } } }")
    return [(found[f"p{i}"] or {}).get("body") or "" for i in range(len(protocols))]


protocols = [p for p in workspace(LIST)["skills"] if p["kind"] == "WORKSPACE"]
readable = [p for p in protocols if p["status"] != "MERGED"]
bodies = {}
for start in range(0, len(readable), BATCH):
    batch = readable[start : start + BATCH]
    for p, body in zip(batch, read_bodies(batch)):
        bodies[(p["taskType"], p["name"])] = body

with open("protocols.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.writer(file)
    writer.writerow(
        ["task_type", "name", "status", "merged_into", "tags",
         "description", "updated_at", "body"]
    )
    for p in protocols:
        writer.writerow(
            [p["taskType"], p["name"], p["status"], p["mergedInto"] or "",
             ";".join(p["tags"]), p["description"], p["updatedAt"],
             bodies.get((p["taskType"], p["name"]), "")]
        )

print(f"Wrote {len(protocols)} protocols to protocols.csv")
```

**2. Run it**

```bash theme={null}
FILED_TOKEN=YOUR_WORKSPACE_TOKEN python3 export_protocols.py
```

```text Output theme={null}
Wrote 12 protocols to protocols.csv
```

**3. Open protocols.csv**

**CSV columns**

**task\_type** `string`

The task family, such as `tax-prep` or `tax-advisor`.

**name** `string`

The protocol's name.

**status** `string`

`APPROVED`, `DISABLED`, `PENDING` or `MERGED`.

**merged\_into** `string`

For a merged protocol, the one it was folded into.

**tags** `string`

The firm's tags, separated by `;`.

**description** `string`

A one-line summary.

**updated\_at** `string`

ISO 8601 time of the last change.

**body** `string`

The protocol's Markdown. Empty for a merged protocol.

> [!WARNING]
>
> An OAuth access token lasts three minutes. If it expires mid-export,
> [refresh it](https://developers.apps.filed.com/guides/connect-with-oauth.md#refreshing) and run the script again.

## 4. Turn a protocol on or off

Workspace admins can turn a firm protocol off without deleting it, and back on.

```graphql Mutation theme={null}
mutation SetProtocolActive($taskType: String!, $name: String!, $active: Boolean!) {
  setSkillActive(taskType: $taskType, name: $name, active: $active) {
    name
    status
  }
}
```

```json Variables theme={null}
{
  "taskType": "tax-prep",
  "name": "consolidated-brokerage-1099s",
  "active": false
}
```

```json Response theme={null}
{
  "data": {
    "setSkillActive": {
      "name": "consolidated-brokerage-1099s",
      "status": "DISABLED"
    }
  }
}
```

The protocol's `status` becomes `DISABLED`, or `APPROVED` when turned back on.

| Who calls it | Result |
| - | - |
| A workspace admin | The new `status` |
| A user who is not an admin | `FORBIDDEN`, `admin role required` |
| An OAuth grant without a write scope | `FORBIDDEN`, refused as read-only |

## What the API can't do yet

The API cannot create a protocol or change its text. Firms write and edit
protocols in the Filed web app, under **Playbook**, with the analyst. Through
the API you can list, read, turn on or off, [promote, approve and
delete](https://developers.apps.filed.com/apis/skills.md) protocols.

Each workspace keeps its own protocols, and the API cannot copy a protocol
from one workspace into another.

Filed's built-in protocols aren't returned, and neither are a firm's edits to
them: the export covers protocols the firm wrote itself.


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