> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gumloop.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Run Gumloop agents, chat completions, MCP tools, Company Brain, skills, and artifacts from your own Python code.

The `gumloop` package lets you call Gumloop from Python. The same agents you build in the app can be started, streamed, and managed from a script, a backend service, or a CI job. The package also installs the [`gumloop` CLI](/cli/overview), and every CLI command runs through the SDK.

For raw HTTP, see the [API reference](/api-reference/getting-started).

## Overview

The package ships two clients:

| Client | What it does | When to use |
| - | - | - |
| **`Gumloop`** | The resource client: agents, sessions, chat completions, models, MCP, Company Brain, skills, artifacts, teams, evaluations, and browser profiles. | All new code. |
| **`AsyncGumloop`** | Async mirror of `Gumloop`, with the same resources and arguments. | Servers, bots, and running many calls at once. |
| **`GumloopClient`** | The legacy flows client. Starts a saved workflow and polls for its outputs. Emits a `DeprecationWarning` when you create it. | Only for `run_flow` against an existing saved workflow. |

## Core concepts

| Concept | Description |
| - | - |
| **Agent** | An AI assistant with a model, instructions, tools, and skills. `client.agents` creates and configures agents. |
| **Session** | One conversation with one agent. `client.sessions` starts it, streams it, sends follow-ups, and cancels it. |
| **StreamEvent** | One event from a streamed session, with `type`, `data`, `stream_cursor`, and `final`. |
| **Chat completion** | A single model call, outside of any agent. `client.chat.completions` uses the OpenAI request shape. |
| **MCP server** | A connected integration, such as Gmail or Slack. `client.mcp` lists its tools and runs them. |

## Installation

```bash theme={"dark"}
pip install gumloop
```

Or with uv:

```bash theme={"dark"}
uv add gumloop
```

Requires Python 3.10 or later. The SDK works on Windows, macOS, and Linux. (The CLI that ships in the same package is macOS and Linux only.)

## Authentication

Every client needs a credential. Pick one:

| Credential | How to pass it | Use when |
| - | - | - |
| **Personal API key** | `api_key=` plus `user_id=` | Scripts and services you control. |
| **Team API key** | `api_key=` plus `user_id=` and `team_id=` | Team automations, server-to-server, CI. |
| **OAuth access token** | `access_token=` | Apps that other Gumloop users sign in to. |

Your API key is in the **Gumloop API key** row of your [Profile settings page](https://www.gumloop.com/settings/profile/general), and your user ID is in the **User ID** row just above it. See [Find your Gumloop API key](/help/connectors/find-your-api-key) and [Personal vs team keys](/api-reference/authentication#personal-vs-team-keys).

```python theme={"dark"}
from gumloop import Gumloop

client = Gumloop(api_key="your_api_key", user_id="your_user_id")
```

### Team API keys

Team keys are scoped to one team. Pass the team's ID and the user ID of the member you're acting as. Every request is checked against that team and only reaches resources the team owns.

```python theme={"dark"}
client = Gumloop(
    api_key="your_team_api_key",
    user_id="your_user_id",  # must be a member of the team
    team_id="your_team_id",
)

agents = client.agents.list()  # scoped to the team
```

### Environment variables

Any argument you leave out is read from the environment:

| Variable | Used for |
| - | - |
| `GUMLOOP_ACCESS_TOKEN` | The bearer token, when you pass neither `api_key` nor `access_token`. Accepts an API key or an OAuth access token. The client re-reads it on every request, so a rotated value is picked up without rebuilding the client. |
| `GUMLOOP_USER_ID` | `user_id` |
| `GUMLOOP_TEAM_ID` | `team_id` |
| `GUMLOOP_BASE_URL` | `base_url`. Defaults to `https://api.gumloop.com/api/v1`. |
| `GUMLOOP_STREAM_BASE_URL` | `stream_base_url`, the host used for streaming and chat completions. Defaults to `base_url` with `api.gumloop.com` swapped for `ws.gumloop.com`. |

```bash theme={"dark"}
export GUMLOOP_ACCESS_TOKEN="your_api_key"
export GUMLOOP_USER_ID="your_user_id"
```

```python theme={"dark"}
client = Gumloop()  # reads the variables above
```

> [!NOTE]
>
> `GUMLOOP_API_KEY` is read by the [CLI](/cli/authentication#environment-variables), not by the SDK. In Python, use `GUMLOOP_ACCESS_TOKEN` or pass `api_key=`.

If no credential is found, the first request raises `AuthenticationError`.

### OAuth

`client.oauth` has the OAuth 2.0 + PKCE helpers for apps that other Gumloop users sign in to. The OAuth flow itself is covered in [OAuth](/api-reference/oauth).

```python theme={"dark"}
from gumloop import Gumloop

oauth = Gumloop().oauth
redirect_uri = "http://127.0.0.1:8765/callback"

registration = oauth.register_client(redirect_uri, client_name="My app")
url, code_verifier, state = oauth.build_authorization_url(registration["client_id"], redirect_uri)
# Send the user to `url`. Gumloop redirects back to redirect_uri with ?code=...&state=...

tokens = oauth.exchange_code(registration["client_id"], code, redirect_uri, code_verifier)
client = Gumloop(access_token=tokens["access_token"])

# Later:
tokens = oauth.refresh_token(registration["client_id"], tokens["refresh_token"])
oauth.revoke(registration["client_id"], tokens["access_token"])
```

The default scopes are `gumloop` and `userinfo`. Pass `scopes=` to `register_client` and `build_authorization_url` to change them.

> [!NOTE]
>
> OAuth access to the Gumloop API requires the [Pro plan or above](https://www.gumloop.com/pricing). A token for an account below Pro is refused with `403` and the error `tier_required_pro`. API keys don't have this plan check.

## Quick start

List your agents, start a session with one, and stream the reply:

```python theme={"dark"}
from gumloop import Gumloop

client = Gumloop(api_key="your_api_key", user_id="your_user_id")

agent = client.agents.list().agents[0]

for event in client.sessions.stream(agent.id, input="Hello, what can you do?"):
    print(event.type, event.data)
    if event.final:
        break
```

## Async usage

`AsyncGumloop` takes the same arguments and has the same resources as `Gumloop`. Await each call, and use `async for` on streams.

```python theme={"dark"}
import asyncio

from gumloop import AsyncGumloop


async def main() -> None:
    async with AsyncGumloop(api_key="your_api_key", user_id="your_user_id") as client:
        models, agents = await asyncio.gather(
            client.models.list(),
            client.agents.list(),
        )
        agent = agents.agents[0]

        async for event in client.sessions.stream(agent.id, input="Hello, what can you do?"):
            print(event.type, event.data)
            if event.final:
                break


asyncio.run(main())
```

## Client options

| Argument | Default | Description |
| - | - | - |
| `api_key` | — | Gumloop API key. Sent as the bearer token. |
| `access_token` | `GUMLOOP_ACCESS_TOKEN` | OAuth access token. Takes priority over `api_key`. |
| `user_id` | `GUMLOOP_USER_ID` | Your Gumloop user ID. Needed with API keys. |
| `team_id` | `GUMLOOP_TEAM_ID` | Scopes every request to one team. Needed with team keys. |
| `base_url` | `https://api.gumloop.com/api/v1` | API host. |
| `stream_base_url` | Derived from `base_url` | Host for streams and chat completions. |
| `timeout` | `90.0` | Seconds before a regular request times out. |
| `stream_timeout` | `3600.0` | Seconds before a stream or chat completion times out. Pass `None` for no limit. |

Both clients hold an HTTP connection pool. Use them as context managers (`with Gumloop(...) as client:` or `async with AsyncGumloop(...) as client:`), or call `client.close()` / `await client.aclose()` when you're done.

## Run an agent

A session is one conversation with an agent. You can wait for the agent's reply, or stream it as it's generated.

### Start a session

`client.sessions.create(agent_id, input=...)` starts a session and returns a `SessionResponse`. The agent may still be working when it returns, so poll `retrieve` until the session's `state` is `completed` or `failed`.

```python theme={"dark"}
import time

from gumloop import Gumloop

client = Gumloop(api_key="your_api_key", user_id="your_user_id")

response = client.sessions.create(
    "agent_id",
    input="Summarize this week's pipeline.",
    name="Weekly pipeline",          # optional
    metadata={"source": "cron"},     # optional
)
session_id = response.session.id

session = client.sessions.retrieve(session_id).session
while session.state not in ("completed", "failed"):
    time.sleep(2)
    session = client.sessions.retrieve(session_id).session

for message in session.messages:
    print(message.role, message.content)
print(session.usage.credit_cost if session.usage else None)
```

| Argument | Description |
| - | - |
| `input` | The first user message. |
| `session_id` | Pick the session ID yourself. Otherwise the server assigns one. |
| `name` | Session name. |
| `metadata` | Your own key-value data to store on the session. |
| `incognito` | Start an incognito session. |
| `stream` | `True` returns an iterator of `StreamEvent` instead of a `SessionResponse`. |

A `Session` carries `id`, `agent_id`, `name`, `type`, `state`, `messages`, `created_at`, `usage` (`credit_cost`, `input_tokens`, `output_tokens`, and more), and `incognito`. `SessionResponse.queue_position` is set when the session is waiting in a queue.

### Stream a session

`client.sessions.stream(agent_id, input=...)` starts a session and yields `StreamEvent` objects as the agent works. It's the same as `create(..., stream=True)`.

```python theme={"dark"}
last_cursor = None
for event in client.sessions.stream("agent_id", input="Write a haiku about Toronto."):
    last_cursor = event.stream_cursor or last_cursor
    print(event.type, event.data)
    if event.error_message:
        print("Error:", event.error_message)
    if event.final:
        break
```

Each `StreamEvent` has:

| Field | Description |
| - | - |
| `type` | The event type. |
| `data` | The event payload. |
| `stream_cursor` | Position in the stream. Save the latest one to resume later. |
| `final` | `True` on the last event. |
| `finish_reason` | Why the run stopped, on the final event. |
| `error`, `error_message` | Set when the run hit an error. |

If your connection drops, pick the stream back up from the last cursor you saw:

```python theme={"dark"}
for event in client.sessions.resume_stream(session_id, last_cursor):
    print(event.type, event.data)
```

### Send a follow-up

```python theme={"dark"}
response = client.sessions.send(session_id, input="Now make it shorter.")

for event in client.sessions.stream_message(session_id, input="And translate it to French."):
    print(event.type, event.data)
```

`send` takes `stream=True` too. Both raise `ValueError` if you don't pass `input`.

### List, rename, and cancel

```python theme={"dark"}
page = client.sessions.list("agent_id", state="completed", page_size=20)
for s in page.sessions:
    print(s.id, s.name, s.state)
# page.next_cursor -> pass as cursor= for the next page

client.sessions.update(session_id, name="Pipeline summary")
client.sessions.cancel(session_id)
```

`list` also filters by `search`, `type` (for example `api`, `chat`, or `slack`), `creator_user_id`, `trigger_id`, and `sort_order`.

### Incognito sessions

Pass `incognito=True` to `create` or `stream` for an incognito session. To make every session on an agent incognito, call `client.agents.set_incognito(agent_id, True)`. That needs the **Incognito agents** role permission, and fails with `organization_permission_required` without it.

## Chat completions

`client.chat.completions.create(...)` is an OpenAI-compatible chat surface that routes to every model Gumloop supports (Anthropic, OpenAI, Google Gemini, OpenRouter routes). The streaming variant returns an iterator of `ChatStreamChunk`, and the unary variant returns a `ChatResult`.

> [!NOTE]
>
> `model` must be a concrete model. `gumloop-chew`, the [**Auto** router](/core-concepts/ai_models#auto-let-gumloop-choose-the-model), picks a model instead of being one. Use `client.models.route(...)` (or `POST /models/route`) to get the model it would choose, then pass that here. Requires a current `gumloop` package that includes the models resource.

```python theme={"dark"}
from gumloop import Gumloop

client = Gumloop(access_token="your_access_token")

result = client.chat.completions.create(
    model="claude-sonnet-4-5",
    messages=[{"role": "user", "content": "Capital of Canada?"}],
)
print(result.choices[0].message.content)
```

### Streaming

```python theme={"dark"}
for chunk in client.chat.completions.create(
    model="claude-sonnet-4-5",
    messages=[{"role": "user", "content": "Write a haiku about Toronto."}],
    stream=True,
):
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)
```

### Structured output

Pass `response_format={"type": "json_schema", "json_schema": {...}}` to constrain the response to a JSON Schema. The SDK accepts the same shape the OpenAI API documents.

```python theme={"dark"}
result = client.chat.completions.create(
    model="claude-sonnet-4-5",
    messages=[{"role": "user", "content": "Return JSON with the capital of Canada."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "answer",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {"capital": {"type": "string"}},
                "required": ["capital"],
                "additionalProperties": False,
            },
        },
    },
)
print(result.choices[0].message.content)  # '{"capital":"Ottawa"}'
```

### Image generation

Request an image-generation model such as `gpt-image-2.5-flare` or `gemini-*-image-preview` with `modalities=["image", "text"]`. The response carries image attachments on `choices[0].message.images` as data URLs. Streaming variants emit partial frames natively for OpenAI gpt-image models.

```python theme={"dark"}
result = client.chat.completions.create(
    model="gpt-image-2.5-flare",
    messages=[{"role": "user", "content": "A red maple leaf on white"}],
    modalities=["image", "text"],
    image_config={"size": "1024x1024"},
)
for image in result.choices[0].message.images:
    print(image.image_url.url[:64], "...")
```

### Tool calling

Pass OpenAI-shape tool definitions. The SDK forwards them unchanged so any LLM that supports function calling can invoke them.

```python theme={"dark"}
result = client.chat.completions.create(
    model="claude-sonnet-4-5",
    messages=[{"role": "user", "content": "What's the weather in Toronto?"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    }],
)
for tc in result.choices[0].message.tool_calls or []:
    print(tc.function.name, tc.function.arguments)
```

### Bring your own provider key

When the calling user has configured their own provider API key (OpenAI, Anthropic, etc.) in Gumloop, billing follows that account's pricing policy. Usage pricing can waive inference for BYOK, and some legacy organizations use a partial discount. See [Credits](/core-concepts/credits). You don't need to change any SDK code.

## Pick a model with Auto

`client.models.route(...)` asks [Auto](/core-concepts/ai_models#auto-let-gumloop-choose-the-model) (`gumloop-chew`) which model should handle a task. It only returns the decision: nothing runs.

```python theme={"dark"}
decision = client.models.route(
    input="Summarize this email thread and draft a reply",
    models=["gpt-5.6-luna", "x-ai/grok-4.6", "claude-opus-5"],
)

print(decision.route.model, decision.route.lane, decision.route.fallback_models)
```

Leave out `models` to route across the full Auto catalog. `client.models.list()` returns the models you can use, grouped as they are in the app.

## MCP tools

`client.mcp.execute(...)` returns an `McpExecuteResponse` with one result per tool call. The SDK reports MCP execution failures, such as target server authentication errors or upstream connection failures, on each result instead of raising `APIStatusError`. Gumloop request errors, such as missing credentials, invalid request bodies, or endpoint permission failures, can still raise `APIStatusError`.

```python theme={"dark"}
from gumloop import APIStatusError, Gumloop

client = Gumloop(access_token="your_access_token")

try:
    response = client.mcp.execute(
        server_id="gumloop_slack",
        tool_name="slack_send_message",
        arguments={"channel": "#general", "text": "Hello from Gumloop"},
    )
except APIStatusError as error:
    print(f"Gumloop API request failed: {error}")
    raise

result = response.results[0]
if result.status != "success":
    print(result.error)
else:
    print(result.decoded_content)
```

`decoded_content` is a Python SDK convenience property. Raw REST responses include the underlying `content` array.

### Servers, tools, and accounts

```python theme={"dark"}
for server in client.mcp.list_servers().servers:
    print(server.server_id, server.name, server.status)

for tool in client.mcp.list_tools("server_id").tools:
    print(tool.name, tool.description)

for account in client.mcp.list_accounts("server_id").accounts:
    print(account.id, account.label, account.is_default)

result = client.mcp.execute(
    server_id="server_id",
    tool_name="tool_name",
    arguments={},
    account="account_id",  # optional; uses the default account when omitted
)
```

When you have more than one account connected to a server, `list_accounts` returns each one's `id`, `label`, and `is_default`. Pass `account=` to pick one. Without it, the default account is used. `client.mcp.execute_many([...])` runs several calls in one request and returns one result per call, in order. `list_resources`, `get_resource`, `list_prompts`, and `get_prompt` read a server's MCP resources and prompts.

## Agent configuration

Everything the agent panel can set is available through `client.agents`. `create` takes the whole configuration in one call. After that, settings change through `update`, and each collection has its own attach/detach pair so one item changes without re-sending the rest.

```python theme={"dark"}
from gumloop import Gumloop

client = Gumloop(access_token="your_access_token")

agent = client.agents.create(
    name="Support triage",
    model_name="anthropic/claude-sonnet-4",
    system_prompt="Triage incoming tickets and draft replies.",
    skill_ids=["skill_2b9c"],
    knowledge_sources=[{"connector_id": "PFqdAMir8PA2Xc6qcszSN9"}],  # omit config for the whole source
    metadata={"fallback": {"enabled": True}, "max_steps": 30},
).agent

client.agents.attach_mcp_server(agent.id, "gmail", approval_mode="write", restricted_tools=["send_email"])
client.agents.update_abilities(agent.id, web_search={"enabled": True, "provider": "exa"}, ask_question={"enabled": False})
client.agents.attach_subagent(agent.id, "xyz789MNOpqrSTU")
client.agents.create_trigger(agent.id, type="schedule", prompt="Morning digest", cron_expression="0 9 * * 1-5")
```

One `retrieve` returns the whole agent: `tools`, `metadata`, `abilities`, `skill_ids`, `knowledge_sources`, `triggers` and `version`. Pass that `version` back on `update` and the call fails with `agent_version_conflict` if someone else saved the agent in between. Leave it out and the last write wins.

```python theme={"dark"}
agent = client.agents.retrieve(agent.id).agent
client.agents.update(agent.id, version=agent.version, metadata={"voice": {"enabled": True}})
```

A webhook trigger's URL embeds its secret. It comes back once on `create_trigger` and from `get_trigger_webhook_url`, never in `list_triggers` or `retrieve`. `delete` removes a custom agent. You can read and chat with `gumball` and `analytics`, but not configure them.

## Agent versions

Every agent keeps an immutable history of its configuration. `client.agents.list_versions(...)` pages through those snapshots newest-first, and `client.agents.get_version(...)` returns one version's full configuration plus the structured diff against the version before it.

```python theme={"dark"}
from gumloop import Gumloop

client = Gumloop(access_token="your_access_token")

response = client.agents.list_versions("abc123DEFghiJKL", page_size=20)
for version in response.versions:
    print(version.id, version.major_version, version.is_deployed, version.created_at)

detail = client.agents.get_version("abc123DEFghiJKL", response.versions[0].id)
print(detail.version.composition.model_name)
print(detail.version.composition.system_prompt)
print(detail.changes)  # None for the agent's first version
```

Both calls require configuration access on the agent and only read data. Deploying or restoring a version has to happen in the app.

## Evaluations

`client.agents.list_evaluations(...)` pages through an agent's graded chats newest-first, `client.agents.get_evaluation_metrics(...)` returns grade and tag counts over a window, and `client.agents.run_evaluations(...)` queues evaluations for finished sessions. `client.agents.get_evaluation_options()` lists the criterion types, priorities, and limits the config accepts.

```python theme={"dark"}
from datetime import datetime, timedelta, timezone

from gumloop import Gumloop

client = Gumloop(access_token="your_access_token")

since = datetime.now(timezone.utc) - timedelta(days=7)
failed = client.agents.list_evaluations("abc123DEFghiJKL", grade="needs_attention", created_after=since)
for evaluation in failed.evaluations:
    print(evaluation.interaction_id, evaluation.summary)

metrics = client.agents.get_evaluation_metrics("abc123DEFghiJKL", days=30)
print(metrics.grades)

run = client.agents.run_evaluations("abc123DEFghiJKL", session_ids=[e.interaction_id for e in failed.evaluations])
for queued in run.results:
    print(queued.session_id, queued.id)
for skipped in run.skipped:
    print(skipped.session_id, skipped.reason)
```

Listing and metrics need read access on the agent and cover the chats you can see. Running evaluations needs edit access and consumes one credit per session. Pass `dry_run=True` to see the cost and the sessions it would skip (unfinished, or already queued) without queuing anything. To list results from an organization evaluation enforced on the agent, call `list_evaluations(agent_id, organization_evaluation_id=...)`.

## Organization evaluations

`client.evaluations` manages rubrics an organization admin defines once and applies to teams, people, or individual agents (Enterprise plan, **Manage evaluations** permission). A new evaluation starts disabled: set its targets, then enable it. Run it over past sessions with `run(...)`. Grading is asynchronous, so poll each returned result id.

```python theme={"dark"}
import time

from gumloop import Gumloop

client = Gumloop(access_token="your_access_token")

organization_id = client.organizations.list().organizations[0].id

created = client.evaluations.create(
    organization_id=organization_id,
    name="Support tone",
    config={
        "criteria": [
            {"name": "Greets the customer", "prompt": "Did the agent greet the customer by name?", "priority": "needs_review"}
        ]
    },
)
evaluation_id = created.evaluation.id

client.evaluations.set_targets(evaluation_id, [{"type": "team", "id": "team_abc"}])
client.evaluations.update(evaluation_id, enabled=True)

queued = client.evaluations.run(evaluation_id, ["session_1", "session_2"])
for result in queued.results:
    polled = client.evaluations.get_result(evaluation_id, result.id).result
    while polled.status in ("queued", "in_progress"):
        time.sleep(5)
        polled = client.evaluations.get_result(evaluation_id, result.id).result
    print(result.session_id, polled.status, polled.grade)

for result in client.evaluations.list_results(evaluation_id, grade="needs_attention").results:
    print(result.agent_id, result.session_id, result.summary)
```

`client.evaluations.options()` returns the size limits and the allowed values for every enum field: grades, statuses, target types, session types, and criterion priorities.

## Company Brain

`client.brain.search(...)` runs a hybrid (semantic + keyword) search across the knowledge sources indexed in your [Company Brain](/core-concepts/brain) and returns ranked results scoped to what you can access. Brain is available on the Pro and Enterprise plans, and each search consumes credits.

```python theme={"dark"}
from gumloop import Gumloop

client = Gumloop(access_token="your_access_token")

response = client.brain.search(
    "what is our refund policy?",
    limit=8,                                 # optional, 1–50 (default 8)
    source_type=["notion", "google_drive"],  # optional filter
)
for result in response.results:
    print(result.score, result.source, result.title, result.url)
```

Each result carries `document_id`, `source`, `title`, `content`, `url`, `score`, `updated_at`, `owner_name`, `owner_email`, `parent_title`, and `metadata`. Omit `source_type` to search every source you can access. Valid values are `notion`, `google_drive`, `slack`, `github`, `confluence`, `direct_file_uploads`, and `gumloop_artifacts`.

### Sources and files

Create file-upload sources and put files into them. The SDK can only create `direct_file_uploads` sources. You set up connected sources in the app.

```python theme={"dark"}
source = client.brain.create_source("Engineering docs").source            # personal, active
# client.brain.create_source("Runbooks", scope="team", team_id="...")
# client.brain.create_source("Policies", scope="organization", require_approval=True)

upload = client.brain.upload_files(source.id, {"handbook.pdf": open("handbook.pdf", "rb").read()})
print([f.status for f in upload.files], upload.rejected)                    # indexing starts on its own

for f in client.brain.list_files(source.id).files:
    print(f.file_name, f.status, f.sha256)                                  # uploaded → indexing → indexed

client.brain.delete_file(source.id, upload.files[0].id)
client.brain.delete_source(source.id)
```

`list_sources(scope=..., source_type=..., team_id=..., page_size=..., cursor=...)` and `get_source(id)` read sources of every type. A source created with `require_approval=True` is a draft: `get_estimate(id).estimate.estimated_credits` shows the cost once `estimate.status` is `paused_for_approval`, and `approve_source(id)` starts indexing. Every file's `sha256` lets you diff a local folder against a source. The CLI's [`gumloop brain sync`](/cli/brain) does exactly that.

## Skills

`client.skills` uploads, updates, downloads, and deletes [skills](/core-concepts/skills). Files are passed as a `{filename: content}` mapping and must include a `SKILL.md`.

```python theme={"dark"}
from pathlib import Path

skill = client.skills.create(
    {"SKILL.md": Path("SKILL.md").read_text()},
    team_id="your_team_id",  # optional; defaults to the client's team_id
).skill

client.skills.update(skill.id, {"SKILL.md": Path("SKILL.md").read_text()})

download = client.skills.download(skill.id)
print(download.download_url, download.filename)  # a zip

client.agents.attach_skills("agent_id", skill.id)
client.skills.delete(skill.id)
```

`client.skills.list(...)` filters by `team_id`, `search_query`, `creator_user_id`, `related_server_id`, and `agent_id`, and pages with `page_size` and `cursor`.

## Artifacts

[Artifacts](/core-concepts/agent_artifacts) are the files agents produce. List them per agent or per session, then download one through a signed URL.

```python theme={"dark"}
page = client.artifacts.list("agent_id", session_id="session_id")
for artifact in page.artifacts:
    print(artifact.id, artifact.filename)

download = client.artifacts.download(page.artifacts[0].id)
print(download.download_url, download.filename, download.size)
```

Pass `version_id=` to `download` for an older version. `client.artifacts.set_password(artifact_id, "...")` protects an artifact's share link with a password.

## Sharing

Sessions, skills, and artifacts share the same access methods: `get_access(id)` and `update_access(id, share=..., remove=..., link=...)`.

```python theme={"dark"}
client.skills.update_access(
    skill.id,
    share=[
        {"type": "person", "email": "teammate@example.com", "role": "editor"},
        {"type": "team", "team_id": "your_team_id", "role": "viewer"},
    ],
    remove=[{"type": "person", "email": "former@example.com"}],
    link={"mode": "anyone"},
)

access = client.sessions.get_access("session_id").access
print(access.people, access.teams, access.link, access.link_role)
```

Share entries are `person` (with `email`), `team` (with `team_id`), or `organization`. Link modes are `none`, `anyone`, and `password`. Leave out the link `role` to use the item's default. The roles each item accepts are listed in [Sharing roles](/help/sharing/sharing-roles).

## Teams and organizations

```python theme={"dark"}
for team in client.teams.list().teams:
    print(team.id, team.name)

for org in client.organizations.list().organizations:
    print(org.id, org.name)
```

## Browser profiles

Agents that use the Browser ability keep their sign-ins in a browser profile. `client.browser_profiles.list()` lists the profiles you can use, and `client.browser_profiles.import_cookies(profile_id, cookies=[...], url=...)` adds cookies to one. Pass `team_id=` for a team profile. The CLI's `gumloop browser import-logins` reads cookies from your local browser and calls this for you.

## Errors

Every SDK error subclasses `GumloopError`:

| Error | Raised when |
| - | - |
| `AuthenticationError` | No credential was found (no `api_key`, `access_token`, or `GUMLOOP_ACCESS_TOKEN`). |
| `APIStatusError` | Gumloop returned an HTTP error status. |
| `GumloopError` | Other SDK failures, such as an invalid chat completion request. |

`APIStatusError` has the details of the failed request:

| Attribute | Description |
| - | - |
| `status_code` | HTTP status, for example `401`, `403`, or `404`. |
| `code` | Machine-readable error code, for example `agent_version_conflict` or `tier_required_pro`. |
| `details` | Extra context from the error body, as a dict. |
| `body` | The raw response body. |

```python theme={"dark"}
from gumloop import APIStatusError, AuthenticationError, Gumloop

client = Gumloop()

try:
    client.agents.retrieve("agent_id")
except AuthenticationError:
    print("Set GUMLOOP_ACCESS_TOKEN or pass api_key=")
except APIStatusError as error:
    print(error.status_code, error.code, error)
```

## Flows (legacy client)

```python theme={"dark"}
from gumloop import GumloopClient

# Initialize the client
client = GumloopClient(
    api_key="your_api_key",
    user_id="your_user_id"
)

# Run a flow and wait for outputs
output = client.run_flow(
    flow_id="your_flow_id",
    inputs={
        "recipient": "example@email.com",
        "subject": "Hello",
        "body": "World"
    }
)

print(output)
```

Optionally add a `project_id` when creating the client if running automations in a workspace:

```python theme={"dark"}
from gumloop import GumloopClient

# Initialize the client
client = GumloopClient(
    api_key="your_api_key",
    user_id="your_user_id",
    project_id="your_project_id"
)
```
