Apps and credentials
Every app in a run answers its vendor's real API at the run's own address. Point the client you
already use at base_url (or the endpoint named below), give it the run's token, and leave the rest of
your code as it is.
The examples read a run from STATEWEAVE_RUN_FILE with this helper. With the Python SDK, use
run.apps and run.credential(app, kind) instead.
import json
import os
run = json.load(open(os.environ["STATEWEAVE_RUN_FILE"]))
def app(name):
return run["apps"][name]
def credential(name, kind="user"):
"""The first credential of an app whose label starts with kind: user, bot, app, mcp or setup."""
return next(v for label, v in app(name)["credentials"].items() if label.split(":", 1)[0] == kind)Roots end in a slash. Endpoints such as web_api and rest_api
end in /. Append the rest without another one: …/gmail/v1/users/me/messages, not
…/gmail/v1//users/me/messages: a doubled slash is another path, and the apps answer it with an
error.
Real clients, unchanged. Each example below was run against StateWeave. Slack and Google use the vendors' own Python libraries; Salesforce, Shopify and Zendesk are shown over plain HTTP. For simple-salesforce, jsforce, ShopifyAPI, Shopify's Admin API client, Zenpy, node-zendesk and the Node Slack and Google libraries, see Client libraries.
Slack
| Endpoint | Use |
|---|---|
web_api | Web API root, …/api/: append the method, such as
conversations.list |
mcp | Slack's MCP server (see MCP servers) |
Credentials: user:<handle> (xoxp-, acts as the persona),
bot:<name> (xoxb-, the company's bot) and app:<name>
(xapp-, the app-level token for Socket Mode).
from slack_sdk import WebClient
slack = WebClient(token=credential("slack", "user"), base_url=app("slack")["endpoints"]["web_api"])
print(slack.auth_test()["user"])
for channel in slack.conversations_list(limit=20)["channels"]:
print(channel["name"])curl -s -X POST "${WEB_API}conversations.list" -H "Authorization: Bearer $SLACK_USER_TOKEN"Gmail
| Endpoint | Use |
|---|---|
rest_api | Gmail API root, …/gmail/v1/ |
discovery | The discovery document, for Google's client libraries |
batch | Batch requests |
mcp | Gmail's MCP server |
Credentials: user:<handle>, the persona's OAuth access token (ya29.). Google's
libraries take it as Credentials(token). Pass discoveryServiceUrl and
static_discovery=False so the client reads the run's discovery document instead of the copy it ships
with.
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
gmail = build("gmail", "v1", credentials=Credentials(credential("gmail")),
discoveryServiceUrl=app("gmail")["endpoints"]["discovery"], static_discovery=False)
print(gmail.users().getProfile(userId="me").execute()["emailAddress"])
messages = gmail.users().messages().list(userId="me", q="is:unread", maxResults=10).execute()curl -s "${REST_API}users/me/messages?maxResults=10" -H "Authorization: Bearer $GOOGLE_TOKEN"Google Calendar
| Endpoint | Use |
|---|---|
rest_api | Calendar API root, …/calendar/v3/ |
discovery | The discovery document |
batch | Batch requests |
Credentials: user:<handle>, the persona's access token.
calendar = build("calendar", "v3", credentials=Credentials(credential("google_calendar")),
discoveryServiceUrl=app("google_calendar")["endpoints"]["discovery"], static_discovery=False)
for cal in calendar.calendarList().list().execute()["items"]:
print(cal["summary"])curl -s "${REST_API}users/me/calendarList" -H "Authorization: Bearer $GOOGLE_TOKEN"Google Sheets
| Endpoint | Use |
|---|---|
sheets_api | Sheets API, …/v4/spreadsheets |
discovery | The Sheets discovery document |
drive_api | Drive API root, to find spreadsheets by name |
drive_discovery | The Drive discovery document |
Credentials: user:<handle>. The same token opens both Sheets and the Drive files listing.
sheets_app = app("google_sheets")
token = Credentials(credential("google_sheets"))
sheets = build("sheets", "v4", credentials=token,
discoveryServiceUrl=sheets_app["endpoints"]["discovery"], static_discovery=False)
drive = build("drive", "v3", credentials=token,
discoveryServiceUrl=sheets_app["endpoints"]["drive_discovery"], static_discovery=False)
found = drive.files().list(q="mimeType='application/vnd.google-apps.spreadsheet'").execute()["files"]
values = sheets.spreadsheets().values().get(spreadsheetId=found[0]["id"], range="A1:F20").execute()curl -s "$SHEETS_API/$SPREADSHEET_ID?includeGridData=false" -H "Authorization: Bearer $GOOGLE_TOKEN"Google Drive
| Endpoint | Use |
|---|---|
drive_api | Drive API root, …/drive/v3/ |
upload | Uploads, …/upload/drive/v3/files |
discovery | The discovery document |
batch | Batch requests |
Credentials: user:<handle>.
drive = build("drive", "v3", credentials=Credentials(credential("google_drive")),
discoveryServiceUrl=app("google_drive")["endpoints"]["discovery"], static_discovery=False)
for f in drive.files().list(pageSize=20, fields="files(id, name, mimeType)").execute()["files"]:
print(f["name"], f["mimeType"])curl -s "${DRIVE_API}files?pageSize=20" -H "Authorization: Bearer $GOOGLE_TOKEN"Salesforce
| Endpoint | Use |
|---|---|
instance_url | The org's instance URL |
rest_api | REST API root, …/services/data/v68.0/ |
mcp | Salesforce's hosted MCP server (sobject-all) |
Credentials: user:<handle> (the persona's access token, sent as
Authorization: Bearer) and mcp:<handle> (for the MCP server).
import urllib.parse
import urllib.request
sf = app("salesforce")
soql = "SELECT Id, Name, Owner.Name FROM Account ORDER BY Name LIMIT 10"
req = urllib.request.Request(sf["endpoints"]["rest_api"] + "query?q=" + urllib.parse.quote(soql),
headers={"Authorization": f"Bearer {credential('salesforce')}"})
accounts = json.load(urllib.request.urlopen(req))["records"]curl -s -G "${REST_API}query" --data-urlencode "q=SELECT Id, Name FROM Account LIMIT 10" \
-H "Authorization: Bearer $SALESFORCE_TOKEN"Shopify
| Endpoint | Use |
|---|---|
admin_graphql | Admin GraphQL API, …/admin/api/2026-10/graphql.json |
mcp | Shopify's MCP server |
Credentials: app:<name> (shpat_, the store's Admin API access token, sent as
X-Shopify-Access-Token) and setup:<handle> (for the MCP server).
Shopify is served through the GraphQL Admin API. The REST Admin API (paths such as
/admin/api/2026-10/products.json) answers 404 with the header x-twin-not-implemented: 1.
query = "{ shop { name } orders(first: 10, reverse: true) { nodes { name displayFinancialStatus } } }"
req = urllib.request.Request(app("shopify")["endpoints"]["admin_graphql"], method="POST",
data=json.dumps({"query": query}).encode(),
headers={"X-Shopify-Access-Token": credential("shopify", "app"),
"Content-Type": "application/json"})
orders = json.load(urllib.request.urlopen(req))["data"]["orders"]["nodes"]curl -s "$ADMIN_GRAPHQL" -H "X-Shopify-Access-Token: $SHOPIFY_TOKEN" -H "Content-Type: application/json" \
-d '{"query": "{ shop { name } orders(first: 10) { nodes { name } } }"}'Zendesk
| Endpoint | Use |
|---|---|
rest_api | Support API root, …/api/v2/ |
Credentials: user:<handle>, the persona's OAuth token, sent as
Authorization: Bearer. Zendesk has no MCP server in StateWeave.
def zendesk(path):
req = urllib.request.Request(app("zendesk")["endpoints"]["rest_api"] + path,
headers={"Authorization": f"Bearer {credential('zendesk')}"})
return json.load(urllib.request.urlopen(req))
me = zendesk("users/me.json")["user"]
tickets = zendesk("tickets.json?per_page=25")["tickets"]curl -s "${REST_API}tickets.json?per_page=25" -H "Authorization: Bearer $ZENDESK_TOKEN"What the apps do and don't do
- Behavior. Endpoints, response shapes, errors, pagination and rate limits follow the real services, checked against them call by call. Where something could not be checked against the real service, the twin's choice is written down rather than guessed silently.
- Isolation. A run's apps are its own. Two runs never see each other's writes, and a run never changes the scenario.
- No outside effects. Nothing leaves StateWeave: an email "sent" from Gmail lands in the scenario's mailboxes, and an order refund moves no money.
- Twin-only addresses. The control paths a twin uses for itself are not open to your agent's credentials.