A session is a conversation with [Claude Code or Codex](/docs/harnesses) in its own cloud environment. The agent keeps working when you close the page or disconnect.

## Start a session

In the dashboard, describe the task on **Home** and click **New session**. From a terminal:

```bash
ellipsis --model claude-opus-5-5 -e cloud_agent_environment "Add an empty-body test in api-repo. Run it; do not commit."
```

From code:

```python
import os
from ellipsis import Ellipsis

client = Ellipsis(api_key=os.environ["ELLIPSIS_API_TOKEN"])
handle = client.sessions.run(
    environment="cloud_agent_environment",
    budget={"session": 5},
    claude_code={
        "model": "claude-opus-5-5",
        "prompt": "Add an empty-body test in api-repo. Run it; do not commit.",
    },
)
print(handle.id)
```

```typescript
import { Ellipsis } from '@ellipsis-dev/sdk';

const client = new Ellipsis({
  apiKey: process.env.ELLIPSIS_API_TOKEN!,
});
const handle = await client.sessions.run({
  environment: 'cloud_agent_environment',
  budget: { session: 5 },
  claude_code: {
    model: 'claude-opus-5-5',
    prompt:
      'Add an empty-body test in api-repo. Run it; do not commit.',
  },
});
console.log(handle.id);
```

Replace `cloud_agent_environment` and `api-repo` with your [environment](/docs/environments) and repository; without an environment, the session uses the default environment. `budget` caps the session at $5. Keep the session ID to check on it later. To set up the client, see the [Python](/docs/api/python-sdk) or [TypeScript](/docs/api/typescript-sdk) SDK and [API authentication](/docs/api/authentication).

## Follow the work

The dashboard and the CLI show messages and tool calls as they happen. To reattach the CLI to a running session, run `ellipsis session get <session-id> --watch`. From code, wait for the turn that answers your prompt:

```python
session = handle.wait(timeout=900)
print(session.turn.status, session.turn.reason, session.turn.detail)

for record in client.sessions.records(handle.id):
    print(record.kind, record.payload)
```

```typescript
const session = await handle.wait({ timeoutMs: 900_000 });
console.log(session.turn?.status, session.turn?.reason, session.turn?.detail);

for await (const record of await client.sessions.records(handle.id)) {
  console.log(record.kind, record.payload);
}
```

`wait()` returns when the turn reaches a final status: `completed`, `failed`, `stopped`, or `cancelled`. Check `turn.status` before treating the task as done; `reason` and `detail` explain a `failed` or `cancelled` turn. A timeout stops your client from waiting, not the agent. To stream messages as they happen, see the [Python](/docs/api/python-sdk#stream-a-session) or [TypeScript](/docs/api/typescript-sdk#stream-a-session) SDK; for what each status means, see [Lifecycle](/docs/sessions/lifecycle#turn).

### Retrieve a session

```python
handle = client.sessions.handle("your-session-id")
print(handle.session.conversation.state, handle.session.turn)
```

```typescript
const handle = await client.sessions.handle('your-session-id');
console.log(handle.session.conversation.state, handle.session.turn);
```

Get a handle by ID at any time, for example after your application restarts. `turn` is the turn in progress, or the latest one when none is running.

## Send a follow-up

<span id="conversations" />

In the dashboard, type in **Reply to the agent...** and click **Send**. In the CLI, type in the session view. From code:

```python
message = handle.send("Add a test for an invalid content type too.")
session = handle.wait(timeout=900)
print(message.turn_id, session.turn.status)
```

```typescript
const message = await handle.send(
  'Add a test for an invalid content type too.'
);
const session = await handle.wait({ timeoutMs: 900_000 });
console.log(message.turn_id, session.turn?.status);
```

A follow-up continues the same conversation and workspace. The message gets its turn the moment it is received, and `wait()` then waits on that turn; a message sent while a turn is running is answered after it. Only the person who started a session can message it. Sessions started by a mention take replies in their GitHub, Slack, or Linear thread. A closed session can't take follow-ups; `conversation.prompting` says whether a message would be accepted, see [Lifecycle](/docs/sessions/lifecycle#conversation).

## Run a one-off task

<span id="one-shot-tasks" />
<span id="from-scripts" />

```python
task = client.sessions.run(
    environment="cloud_agent_environment",
    claude_code={
        "model": "claude-opus-5-5",
        "prompt": "Run request-validation tests in api-repo and report failures.",
    },
    conversation={"interactive": False},
)
result = task.wait(timeout=900)
print(task.id, result.turn.status, result.conversation.state)
```

```typescript
const task = await client.sessions.run({
  environment: 'cloud_agent_environment',
  claude_code: {
    model: 'claude-opus-5-5',
    prompt:
      'Run request-validation tests in api-repo and report failures.',
  },
  conversation: { interactive: false },
});
const result = await task.wait({ timeoutMs: 900_000 });
console.log(task.id, result.turn?.status, result.conversation.state);
```

Set `conversation.interactive` to `false` when you won't send follow-ups. The conversation closes when its turn ends and its environment is deleted; its messages and results stay available. To save a task and run it again or on a schedule, use [Agents](/docs/agents).

## Inspect the result

```python
diff = client.sessions.diff(handle.id)
for file in diff.files:
    print(file.full_name, file.path, file.patch)
print("Omitted paths:", diff.omitted_paths)

git = client.sessions.git(handle.id)
for repo in git.repos:
    print(repo.full_name, repo.commits, repo.prs)
```

```typescript
const diff = await client.sessions.diff(handle.id);
for (const file of diff.files) {
  console.log(file.full_name, file.path, file.patch);
}
console.log('Omitted paths:', diff.omitted_paths);

const git = await client.sessions.git(handle.id);
for (const repo of git.repos) {
  console.log(repo.full_name, repo.commits, repo.prs);
}
```

Get the session's [uncommitted changes](/docs/api/sessions/get-sessions-id-diff) and the [commits and pull requests](/docs/api/sessions/get-sessions-id-git) it made. Files left out of the diff are listed in `omitted_paths`.

### Cost and records

```python
session = handle.refresh()
print("Cost (USD):", session.cost.total / 100_000)
print("Budget (USD):", session.budget / 100_000)
```

```typescript
const session = await handle.refresh();
console.log('Cost (USD):', session.cost.total / 100_000);
console.log('Budget (USD):', session.budget / 100_000);
```

`cost.total` and `budget` are in thousandths of a cent, so divide by 100,000 for dollars. Follow-ups share the budget. Each turn carries its own `cost` and `tokens`; list them with [`GET /v1/sessions/{id}/turns`](/docs/api/sessions/get-sessions-id-turns). See [Pricing](/docs/pricing) for rates, or [download the session history](/docs/api/sessions/get-sessions-id-download).

## Stop a session

Click **Stop** in the dashboard. In the CLI, type `/stop` in the session view or run `ellipsis session stop <session-id>`. From code:

```python
session = handle.stop()
print(session.turn.status, session.turn.stopped)
```

```typescript
const session = await handle.stop();
console.log(session.turn?.status, session.turn?.stopped);
```

Stopping ends the running turn with `stopped`, not the conversation. The session can still take follow-ups. Stopping a session with no turn queued, preparing, or running fails with `409`. Disconnecting doesn't stop the agent.
