# The TypeScript SDK

> @attn/sdk, the hub API from TypeScript: push flows, start runs and wait for them, drive a live session call by call, and share a screen from a script.

`@attn/sdk` is the hub's client for scripts and tests: the API with types, runs, and live sessions driven call by call with the same semantics as flows. It talks to the hub over HTTP only, like the [CLI](https://attention.run/docs/cli), which is built on it. It isn't published to npm yet: scripts run from a checkout of the attention.run repository, where `@attn/sdk` resolves to the workspace package.

## The client

`createClient` takes the hub's URL and an API token (none is needed on a local hub). The examples name the hosted app; until its API is open to scripts ([Status](https://attention.run/docs#status)), give a local hub's URL instead, such as `http://127.0.0.1:7300`. `hub.api` has every route of the API, typed from the hub's route table; a refusal throws an `ApiError` with the hub's `status`, `code` and message.

```ts
import { createClient } from '@attn/sdk'

const hub = createClient({ url: 'https://app.attention.run', token: process.env.ATTN_TOKEN })

const { version, protocol } = await hub.version()
console.log(`hub ${version}, protocol ${protocol}`)
const { targets } = await hub.api.listTargets()
for (const t of targets) console.log(t.id, t.name, t.config.connector)
```

## Flows and runs

`hub.pushFlow(source)` saves flow JSON under its slug: a new flow, or a new version (an unchanged source saves nothing). `hub.api.createRun` starts a run, and `hub.waitForRun` follows it until it ends, with every step event on the way.

```ts
import { createClient } from '@attn/sdk'

const hub = createClient({ url: 'https://app.attention.run', token: process.env.ATTN_TOKEN })
const { targets } = await hub.api.listTargets()
const target = targets.find((t) => t.name === 'Fixture app')
if (!target) throw new Error('no target named "Fixture app"')

const { flow, created } = await hub.pushFlow({
  flow: 'docs/greeting',
  params: { user: { type: 'string', default: 'demo' } },
  outputs: { greeting: { type: 'string' } },
  steps: [
    { navigate: 'http://127.0.0.1:7310/login' },
    { waitFor: { visible: 'login/sign-in' } },
    { set: { greeting: 'Hello, {{params.user}}' } },
  ],
})
console.log(`${flow.slug} v${flow.latest.version}${created ? ' (new)' : ''}`)

const run = await hub.api.createRun({ body: { flow: flow.slug, targetId: target.id, params: { user: 'Ada' } } })
const ended = await hub.waitForRun(run.id, {
  onStep: (e) => {
    if (e.phase === 'end') console.log(e.status, e.path)
  },
})
console.log(ended.status, ended.outputs)
if (ended.status !== 'passed') process.exit(1)
```

```text
docs/greeting v1 (new)
passed steps[0]
passed steps[1]
passed steps[2]
passed { greeting: 'Hello, Ada' }
```

## Live sessions

`hub.session({ targetId })` opens a session on a target and resolves once it is live. Its methods are the runtime's, with the semantics flows have: each action waits for its element, a wait after an action only looks at frames from after it, and an element found in two places is an error. Selectors, targets and conditions are the flow JSON's, and `visible`, `gone`, `any`, `all`, `not`, `stable` and `changed` build conditions. Close the session when you're done; its recording is kept.

```ts
import { createClient, RuntimeError, visible } from '@attn/sdk'

const hub = createClient({ url: 'https://app.attention.run', token: process.env.ATTN_TOKEN })
const { targets } = await hub.api.listTargets()
const target = targets.find((t) => t.name === 'Fixture app')
if (!target) throw new Error('no target named "Fixture app"')

const s = await hub.session({ targetId: target.id })
try {
  await s.navigate('http://127.0.0.1:7310/login')
  await s.type('demo', { into: 'login/username' })
  await s.type('demo', { into: 'login/password' })
  await s.click('login/sign-in')
  const seen = await s.waitFor(visible('welcome/heading'), { timeout: 5000 })
  const [heading] = seen.matches
  console.log(`signed in; welcome/heading at ${heading?.x},${heading?.y} (score ${heading?.score.toFixed(2)})`)
} catch (e) {
  // A failure says what was missing and the best candidate the matcher saw.
  if (e instanceof RuntimeError) console.error(e.code, e.message, e.best)
  throw e
} finally {
  await s.close()
}
```

| Method | What it does |
|---|---|
| `click(target, { button?, count? })`, `hover(target)` | Wait for the target, then click it (`count: 2` double-clicks, `button: 'right'` right-clicks) or move onto it |
| `type(text, { into?, clear? })`, `press(chord)` | Type text (into a target, clicked first), press a key chord such as `Enter` or `Control+A` |
| `drag(from, to)`, `scroll({ at?, dx?, dy?, until? })`, `navigate(url)` | Drag, scroll, open a URL (Chrome) |
| `waitFor(condition, { timeout? })`, `assert(condition)` | Wait for a condition; `assert` fails with ASSERTION |
| `find(selector, { all? })` | Where the element is on screen, with its state (the variant's label) |
| `observe()` | What is on screen: the library's elements that are visible |
| `readClipboard()` | The text the target copied after the last action started |
| `pin(label?)` | Pin the current frame (kept with the recording) |
| `events()` | What the session's runtime saw: steps, inputs, matches |
| `close()` | End the session |

Every method takes a `signal` too: aborting it cancels the call on the runner and releases held keys and buttons. `hub.attach(sessionId)` drives a session that is already open, such as one you started in the web app.

## Sharing a screen

`hub.share()` makes a VNC server or a Chrome on this machine a target of the hub, and `share.run(flow)` runs a flow on it. See [SSH targets](https://attention.run/docs/ssh-targets#from-a-script).

## Not built yet

Hosted script flows (TypeScript that runs on the hub's runners), generated types for a library's elements (`attn types`), and handlers registered from a script (`s.handle`).

---

Canonical: https://attention.run/docs/sdk · Updated 2026-10-15
