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).

Request access

attention.run is in an invite-only preview. Tell us what you’d automate; we invite people in small batches.

What do you want to automate?

Pick all that apply.

What for?
Tell us more (optional)

We’ll only use this to reply about access. Privacy policy.