Docs
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.
Last updated 15 October 2026
@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, 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), 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.
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.
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)
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.
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.
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).