---
title: Read where an output came from
url: /docs/developers/reference/automation-output-source-read
---

# Read where an output came from

`automation.output-source.read@1`

Tells you where one result of an automation came from: the setup that made it, when it was accepted and the answers it was made with. It reads and changes nothing, and answers only for results you may see. It needs a key with **Read automations**.

## Where a result came from

Replace `YOUR_FEATURE_ID`, `YOUR_RUN_REF` and `YOUR_ITEM_KEY` with the values the record made from the result keeps.

```curl tabs
# A new operationId for each new request; the same one to retry it.
curl https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations \
  -H "Authorization: Bearer gl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "target": {
    "featureId": "YOUR_FEATURE_ID",
    "export": "readOutputSource",
    "contractRef": "automation.output-source.read@1"
  },
  "operationId": "read-source-1",
  "input": {
    "source": {
      "featureId": "YOUR_FEATURE_ID",
      "runRef": "11111111-2222-4333-8444-555555555555",
      "itemKey": "YOUR_ITEM_KEY"
    }
  }
}'
```

```javascript tabs
// A new operationId for each new request; the same one to retry it.
const response = await fetch(
  "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer gl_YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "target": {
        "featureId": "YOUR_FEATURE_ID",
        "export": "readOutputSource",
        "contractRef": "automation.output-source.read@1"
      },
      "operationId": "read-source-1",
      "input": {
        "source": {
          "featureId": "YOUR_FEATURE_ID",
          "runRef": "11111111-2222-4333-8444-555555555555",
          "itemKey": "YOUR_ITEM_KEY"
        }
      }
    }),
  },
);
const receipt = await response.json();
console.log(response.status, receipt);
```

```python tabs
import requests

# A new operationId for each new request; the same one to retry it.
response = requests.post(
    "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations",
    headers={"Authorization": "Bearer gl_YOUR_API_KEY"},
    json={
        "target": {
            "featureId": "YOUR_FEATURE_ID",
            "export": "readOutputSource",
            "contractRef": "automation.output-source.read@1",
        },
        "operationId": "read-source-1",
        "input": {
            "source": {
                "featureId": "YOUR_FEATURE_ID",
                "runRef": "11111111-2222-4333-8444-555555555555",
                "itemKey": "YOUR_ITEM_KEY",
            },
        },
    },
    timeout=30,
)
print(response.status_code, response.json())
```

## Parameters

What goes in `input`:

| Name | Type | Required | Description |
|---|---|---|---|
| `source` | object | Yes | The result to look up, as the record made from it keeps it. |
| `source.featureId` | string | Yes | The feature the run belongs to. |
| `source.runRef` | string (uuid) | Yes | The run that made the result, from [Find automation runs](automation-runs.md). |
| `source.itemKey` | string | Yes | The result's key within that run. |

## Response

A call that worked answers `200` with an `applied` receipt; its `output` holds these fields, among others. `GET /catalog/automation.output-source.read@1` answers the complete shape as JSON Schema.

| Name | Type | Description |
|---|---|---|
| `source` | object | The result looked up. |
| `recipe` | object | The setup the result was made with. Absent when `availability` is `unavailable`. |
| `recipe.presetRef` | string | The built-in setup's reference. |
| `recipe.version` | integer | Its version. |
| `recipe.titleAtAdmission` | string | The name it had when the run began. |
| `brief` | object | The answers the run was started with. Absent when `availability` is `unavailable`. |
| `brief.values` | object | The answers, by question. |
| `brief.lockedPaths` | array of string | Answers that could not be changed for this run. |
| `brief.aiPaths` | array of string | Answers left for the AI to fill in. |
| `admittedAt` | string (date-time) | When the run was accepted. |
| `availability` | `unavailable` | `unavailable` when the setup and answers are no longer kept; absent otherwise. |
| `reason` | object | Why it is unavailable. |
| `reason.code` | string | A code for the reason. |
| `reason.message` | string | The reason in words. |

```json title="Response"
{
  "status": "applied",
  "operationId": "read-source-1",
  "output": {
    "source": {
      "featureId": "juniper-cakes.automations",
      "runRef": "11111111-2222-4333-8444-555555555555",
      "itemKey": "post-1"
    },
    "recipe": {
      "presetRef": "YOUR_PRESET_REF",
      "version": 1,
      "titleAtAdmission": "Weekly special",
      "graphRef": "YOUR_GRAPH_REF",
      "graphDigest": "0000000000000000000000000000000000000000000000000000000000000000"
    },
    "brief": {
      "inputSchemaRef": "YOUR_QUESTIONS_REF",
      "values": {
        "topic": "Lemon cake"
      },
      "lockedPaths": [],
      "aiPaths": [
        "/topic"
      ]
    },
    "admittedAt": "2026-11-13T09:30:00Z"
  }
}
```

## Errors

It reads and changes nothing. Besides the answers every request can get ([Errors](../errors.md)), its receipt can say:

| Status | HTTP | What it means |
|---|---|---|
| `rejected` | 400, or 403 when it's a matter of access | Not done. `diagnostics` says why: each has a `code`, a `path` into what you sent and a `message`. |

The codes in `diagnostics` you're most likely to see:

| Code | Status | What it means |
|---|---|---|
| `AUTOMATION.RUN_NOT_FOUND` | `rejected` | No such run here for this key. |
| `AUTOMATION.SOURCE_UNAVAILABLE` | `rejected` | You may not see this run's results. |
| `AUTOMATION.RUN_MOVED` | `conflict` | The run changed while it was being read. Read it again. |
| `AUTOMATION.GRAPH_NOT_EXECUTABLE` | `rejected` | The automation this run follows can no longer be read, so the run cannot be shown. |
