# Adding evidence

> Write data with the Tidal Control GraphQL API. Upload an evidence file, create an execution task on a control and attach the file, with a full Python example.

Source: https://tidalcontrol.com/docs/api/adding-evidence

Most of this guide covers reading data, but the API can also **write** data through mutations. This page walks through a complete example: a custom integration that attaches an evidence file to a control by creating an execution task and adding the file to it.

> **warning:**

Mutations require a token with **write permissions**. Authenticate with the [Device Authorization Grant](/docs/api/authentication) so the token acts as a user with the **Regular User** or **Super User** role. Read-only [machine-to-machine credentials](/docs/api/authentication) cannot run mutations.

## How it works

Evidence in Tidal Control is a **document** linked to an activity. Attaching evidence to a control is four steps:

1. Find the control you want to add evidence to
2. Upload the evidence file as a document
3. Create an execution task linked to the control
4. Attach the document to the task as evidence

## Step 1: Find the control

Search for the control by name to get its `id`:

```graphql
query FindControl($filter: ControlFilter) {
  controls_paged(first: 1, filter: $filter) {
    edges {
      node {
        id
        sequenceId
        name
      }
    }
  }
}
```

Variables:

```json
{ "filter": { "search": "Access control policy" } }
```

## Step 2: Upload the evidence file

Documents are uploaded through a REST endpoint (not GraphQL), as `multipart/form-data` on the `files` field. Use the same Bearer token:

```bash
curl -s -X POST https://portal.tidalcontrol.com/documents \
  -H "Authorization: Bearer {access_token}" \
  -F "files=@evidence.pdf"
```

The response is an array of the documents you uploaded:

```json
[
  {
    "id": "11111111-1111-1111-1111-111111111111",
    "name": "evidence.pdf",
    "type": "application/pdf"
  }
]
```

Keep the document `id` for the next steps.

> **info:**

The maximum upload size is **128 MB** per request. You can upload several files in one request by repeating the `files` field.

## Step 3: Create an execution task on the control

Create an execution task and link it to the control from step 1 using the `controls` field:

```graphql
mutation CreateExecution($input: ExecutionInput!) {
  addExecution(input: $input) {
    id
    sequenceId
    name
  }
}
```

Variables:

```json
{
  "input": {
    "name": "Quarterly access review",
    "controls": ["22222222-2222-2222-2222-222222222222"]
  }
}
```

Only `name` is required. `controls` takes one or more control `id`s, and you can also set `description`, `expires`, `owners`, and `assets`. The mutation returns the new task's `id`.

## Step 4: Attach the evidence

Attach the uploaded document to the task with `addEvidenceToExecution`:

```graphql
mutation AddEvidence($activityId: ID!, $documentIds: [ID!]!) {
  addEvidenceToExecution(activityId: $activityId, documentIds: $documentIds) {
    id
    sequenceId
    evidence {
      id
      document {
        name
      }
    }
  }
}
```

Variables:

```json
{
  "activityId": "33333333-3333-3333-3333-333333333333",
  "documentIds": ["11111111-1111-1111-1111-111111111111"]
}
```

The evidence now appears on the task, which is linked to your control.

## Complete Python example

This script ties the four steps together. It reuses the `get_token()` and `graphql()` helpers from [Authentication](/docs/api/authentication).

```python

GRAPHQL_URL = "https://portal.tidalcontrol.com/graphql"
DOCUMENTS_URL = "https://portal.tidalcontrol.com/documents"

def find_control(search, access_token):
    query = """
    query FindControl($filter: ControlFilter) {
      controls_paged(first: 1, filter: $filter) {
        edges { node { id sequenceId name } }
      }
    }
    """
    data = graphql(query, {"filter": {"search": search}}, access_token)
    edges = data["controls_paged"]["edges"]
    if not edges:
        raise RuntimeError(f"No control found for '{search}'")
    return edges[0]["node"]

def upload_document(file_path, access_token):
    with open(file_path, "rb") as f:
        r = requests.post(
            DOCUMENTS_URL,
            headers={"Authorization": f"Bearer {access_token}"},
            files={"files": f},
        )
    r.raise_for_status()
    return r.json()[0]  # first uploaded document

def create_execution(name, control_id, access_token):
    mutation = """
    mutation CreateExecution($input: ExecutionInput!) {
      addExecution(input: $input) { id sequenceId name }
    }
    """
    data = graphql(
        mutation,
        {"input": {"name": name, "controls": [control_id]}},
        access_token,
    )
    return data["addExecution"]

def add_evidence(activity_id, document_id, access_token):
    mutation = """
    mutation AddEvidence($activityId: ID!, $documentIds: [ID!]!) {
      addEvidenceToExecution(activityId: $activityId, documentIds: $documentIds) {
        id
        sequenceId
        evidence { id document { name } }
      }
    }
    """
    data = graphql(
        mutation,
        {"activityId": activity_id, "documentIds": [document_id]},
        access_token,
    )
    return data["addEvidenceToExecution"]

if __name__ == "__main__":
    token = get_token()
    access_token = token["access_token"]

    control = find_control("Access control policy", access_token)
    document = upload_document("evidence.pdf", access_token)
    task = create_execution("Quarterly access review", control["id"], access_token)
    result = add_evidence(task["id"], document["id"], access_token)

    print(f"Added '{document['name']}' to task #{result['sequenceId']} on control {control['name']}.")
```

## Attaching evidence to an existing task

If a suitable execution task already exists, skip step 3. Find the task with the `activities_paged` query (see [Exporting tasks](/docs/api/tasks)), then call `addEvidenceToExecution` with its `id`.
