stateweave Docs
Guides

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.

run.py
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

EndpointUse
web_apiWeb API root, …/api/: append the method, such as conversations.list
mcpSlack'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

EndpointUse
rest_apiGmail API root, …/gmail/v1/
discoveryThe discovery document, for Google's client libraries
batchBatch requests
mcpGmail'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

EndpointUse
rest_apiCalendar API root, …/calendar/v3/
discoveryThe discovery document
batchBatch 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

EndpointUse
sheets_apiSheets API, …/v4/spreadsheets
discoveryThe Sheets discovery document
drive_apiDrive API root, to find spreadsheets by name
drive_discoveryThe 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

EndpointUse
drive_apiDrive API root, …/drive/v3/
uploadUploads, …/upload/drive/v3/files
discoveryThe discovery document
batchBatch 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

EndpointUse
instance_urlThe org's instance URL
rest_apiREST API root, …/services/data/v68.0/
mcpSalesforce'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

EndpointUse
admin_graphqlAdmin GraphQL API, …/admin/api/2026-10/graphql.json
mcpShopify'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

EndpointUse
rest_apiSupport 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.