curl --request PUT \
--url https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"expected_revision": 123,
"external_refs": [
{
"account_id": "<string>",
"external_id": "<string>",
"kind": "<string>",
"label": "<string>",
"provider": "<string>"
}
]
}
'import requests
url = "https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs"
payload = {
"expected_revision": 123,
"external_refs": [
{
"account_id": "<string>",
"external_id": "<string>",
"kind": "<string>",
"label": "<string>",
"provider": "<string>"
}
]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
expected_revision: 123,
external_refs: [
{
account_id: '<string>',
external_id: '<string>',
kind: '<string>',
label: '<string>',
provider: '<string>'
}
]
})
};
fetch('https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'expected_revision' => 123,
'external_refs' => [
[
'account_id' => '<string>',
'external_id' => '<string>',
'kind' => '<string>',
'label' => '<string>',
'provider' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs"
payload := strings.NewReader("{\n \"expected_revision\": 123,\n \"external_refs\": [\n {\n \"account_id\": \"<string>\",\n \"external_id\": \"<string>\",\n \"kind\": \"<string>\",\n \"label\": \"<string>\",\n \"provider\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"expected_revision\": 123,\n \"external_refs\": [\n {\n \"account_id\": \"<string>\",\n \"external_id\": \"<string>\",\n \"kind\": \"<string>\",\n \"label\": \"<string>\",\n \"provider\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"expected_revision\": 123,\n \"external_refs\": [\n {\n \"account_id\": \"<string>\",\n \"external_id\": \"<string>\",\n \"kind\": \"<string>\",\n \"label\": \"<string>\",\n \"provider\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"current_decisions": [
{
"applicability": "<string>",
"approval_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"authorized_at": "2023-11-07T05:31:56Z",
"decided_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"decider_kind": "<string>",
"decider_rationale": "<string>",
"decision_class": "<string>",
"evidence_kind": "<string>",
"execution_status": "<string>",
"outcome_label": "<string>",
"outcome_status": "<string>",
"proposed_at": "2023-11-07T05:31:56Z",
"slug": "<string>",
"source": "<string>",
"status": "<string>",
"subject_kind": "<string>",
"subject_label": "<string>",
"superseded_by_decision_slug": "<string>",
"supersedes_decision_slug": "<string>",
"what": "<string>",
"why": "<string>"
}
],
"events": [
{
"actor_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"actor_type": "<string>",
"body": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"workstream_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
],
"following": true,
"phases": [
{
"completed_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"skill_slug": "<string>",
"slug": "<string>",
"sort_order": -1,
"started_at": "2023-11-07T05:31:56Z",
"status": "<string>",
"thread_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"title": "<string>",
"updated_at": "2023-11-07T05:31:56Z",
"workstream_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
],
"recent_decisions": [
{
"applicability": "<string>",
"approval_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"authorized_at": "2023-11-07T05:31:56Z",
"decided_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"decider_kind": "<string>",
"decider_rationale": "<string>",
"decision_class": "<string>",
"evidence_kind": "<string>",
"execution_status": "<string>",
"outcome_label": "<string>",
"outcome_status": "<string>",
"proposed_at": "2023-11-07T05:31:56Z",
"slug": "<string>",
"source": "<string>",
"status": "<string>",
"subject_kind": "<string>",
"subject_label": "<string>",
"superseded_by_decision_slug": "<string>",
"supersedes_decision_slug": "<string>",
"what": "<string>",
"why": "<string>"
}
],
"resources": [
{
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"phase_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"relationship_type": "<string>",
"resource_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"resource_type": "<string>",
"title": "<string>",
"workstream_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
],
"workstream": {
"autonomy_mode": "<string>",
"completed_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z",
"description": "<string>",
"escalation_budget_per_day": -1,
"external_refs": [
{
"account_id": "<string>",
"external_id": "<string>",
"kind": "<string>",
"label": "<string>",
"provider": "<string>"
}
],
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"primary_thread_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"project_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"revision": 123,
"slug": "<string>",
"source_template_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"spent_millicents": 123,
"started_at": "2023-11-07T05:31:56Z",
"status": "<string>",
"title": "<string>",
"token_budget_millicents": 123,
"updated_at": "2023-11-07T05:31:56Z"
}
}{
"code": "not_found",
"details": {},
"message": "<string>"
}SetWorkstreamExternalRefsAPI declares which provider-owned objects a
workstream governs — the machine-readable replacement for a campaign allowlist hand-typed into a knowledge record.
REST only, with no MCP mirror, and the asymmetry is the point: an agent must be able to READ the set of live campaigns it may act on, and must not be able to widen it. The consumer holding the mapping declares scope here; agents read it back on every workstream and ledger read.
curl --request PUT \
--url https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"expected_revision": 123,
"external_refs": [
{
"account_id": "<string>",
"external_id": "<string>",
"kind": "<string>",
"label": "<string>",
"provider": "<string>"
}
]
}
'import requests
url = "https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs"
payload = {
"expected_revision": 123,
"external_refs": [
{
"account_id": "<string>",
"external_id": "<string>",
"kind": "<string>",
"label": "<string>",
"provider": "<string>"
}
]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
expected_revision: 123,
external_refs: [
{
account_id: '<string>',
external_id: '<string>',
kind: '<string>',
label: '<string>',
provider: '<string>'
}
]
})
};
fetch('https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'expected_revision' => 123,
'external_refs' => [
[
'account_id' => '<string>',
'external_id' => '<string>',
'kind' => '<string>',
'label' => '<string>',
'provider' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs"
payload := strings.NewReader("{\n \"expected_revision\": 123,\n \"external_refs\": [\n {\n \"account_id\": \"<string>\",\n \"external_id\": \"<string>\",\n \"kind\": \"<string>\",\n \"label\": \"<string>\",\n \"provider\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"expected_revision\": 123,\n \"external_refs\": [\n {\n \"account_id\": \"<string>\",\n \"external_id\": \"<string>\",\n \"kind\": \"<string>\",\n \"label\": \"<string>\",\n \"provider\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/workstreams/{workstreamSlug}/external-refs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"expected_revision\": 123,\n \"external_refs\": [\n {\n \"account_id\": \"<string>\",\n \"external_id\": \"<string>\",\n \"kind\": \"<string>\",\n \"label\": \"<string>\",\n \"provider\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"current_decisions": [
{
"applicability": "<string>",
"approval_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"authorized_at": "2023-11-07T05:31:56Z",
"decided_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"decider_kind": "<string>",
"decider_rationale": "<string>",
"decision_class": "<string>",
"evidence_kind": "<string>",
"execution_status": "<string>",
"outcome_label": "<string>",
"outcome_status": "<string>",
"proposed_at": "2023-11-07T05:31:56Z",
"slug": "<string>",
"source": "<string>",
"status": "<string>",
"subject_kind": "<string>",
"subject_label": "<string>",
"superseded_by_decision_slug": "<string>",
"supersedes_decision_slug": "<string>",
"what": "<string>",
"why": "<string>"
}
],
"events": [
{
"actor_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"actor_type": "<string>",
"body": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"workstream_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
],
"following": true,
"phases": [
{
"completed_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"skill_slug": "<string>",
"slug": "<string>",
"sort_order": -1,
"started_at": "2023-11-07T05:31:56Z",
"status": "<string>",
"thread_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"title": "<string>",
"updated_at": "2023-11-07T05:31:56Z",
"workstream_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
],
"recent_decisions": [
{
"applicability": "<string>",
"approval_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"authorized_at": "2023-11-07T05:31:56Z",
"decided_by_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"decider_kind": "<string>",
"decider_rationale": "<string>",
"decision_class": "<string>",
"evidence_kind": "<string>",
"execution_status": "<string>",
"outcome_label": "<string>",
"outcome_status": "<string>",
"proposed_at": "2023-11-07T05:31:56Z",
"slug": "<string>",
"source": "<string>",
"status": "<string>",
"subject_kind": "<string>",
"subject_label": "<string>",
"superseded_by_decision_slug": "<string>",
"supersedes_decision_slug": "<string>",
"what": "<string>",
"why": "<string>"
}
],
"resources": [
{
"created_at": "2023-11-07T05:31:56Z",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"phase_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"relationship_type": "<string>",
"resource_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"resource_type": "<string>",
"title": "<string>",
"workstream_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
],
"workstream": {
"autonomy_mode": "<string>",
"completed_at": "2023-11-07T05:31:56Z",
"created_at": "2023-11-07T05:31:56Z",
"description": "<string>",
"escalation_budget_per_day": -1,
"external_refs": [
{
"account_id": "<string>",
"external_id": "<string>",
"kind": "<string>",
"label": "<string>",
"provider": "<string>"
}
],
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"organization_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"primary_thread_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"project_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"revision": 123,
"slug": "<string>",
"source_template_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"spent_millicents": 123,
"started_at": "2023-11-07T05:31:56Z",
"status": "<string>",
"title": "<string>",
"token_budget_millicents": 123,
"updated_at": "2023-11-07T05:31:56Z"
}
}{
"code": "not_found",
"details": {},
"message": "<string>"
}Authorizations
An Erdo API key (erdo_api_...) or scoped token (erdo_token_...).
Path Parameters
Body
Response
Success response
agent receives, on the same read: standing commitments and in-flight authorizations, then the history around them. The operator returning to a workstream and the agent continuing it are answering the same question — what are we operating under? — and a detail page that had to derive its own answer would be a second copy free to disagree with the one steering the work.
Show child attributes
Show child attributes
(newest first), embedded so cards/agents see activity without a second call.
Show child attributes
Show child attributes
detail page can render its follow toggle without a second call (Wave E).
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Operational state machine for multi-step work (campaign launches, reports, monitors).
Show child attributes
Show child attributes

