Skip to content

The client API ​

agentswf/client lets a Bun program reach a workflow's runs in its own process: send events, start runs with args, wait for them and read what they posted. tsc checks each call against the workflow the program imports. This page lists every call, what it returns and what it throws; Programs teaches it with a webhook, a relay and a release script, and packages/contract/src/client.ts has the types, with every field documented.

At a glance ​

What a workflow needs ​

The client takes the workflow itself, not its name: its file's default export, giving file: import.meta.path. The client finds the workflow's project from file, and runs every call on the engine that project installs, which must be the client's own version. A program runs where you would type awf: the same user, HOME and PATH (Programs).

client ​

ts
import { client } from "agentswf/client";

const runs = client();
OptionWhat it isDefault
homethe operator's home, where ~/.awf isthe program's HOME
environmentwhat a launched attempt inherits, as awf emit's environment doesthe program's environment

A client holds no connection and no state; make one and pass it to whatever needs it, so a test can pass a fake instead. Its type, for a function that takes one, is Client.

emit ​

ts
export async function pushed(runs: Client, pr: number, sha: string) {
  const sent = await runs.emit(
    review,
    `pr-${pr}`,
    "pr.pushed",
    { sha, branch: "main" },
    { eventId: `push-${sha}`, source: "ci" },
  );
  if (!sent.ok) throw new Error(`${sent.ref}: ${sent.outcome}, ${sent.why ?? sent.did}`);
  return sent.did; // "started" | "nudged" | "continued" | "none"
}

Sends event name to run id: one of the workflow's own events, which may start the run if the workflow lists it in starts, or a participant's {key}:reply to a run that is there. tsc refuses a name the workflow doesn't declare and data its schema refuses; the client checks both again at run time.

OptionWhat it isDefault
eventIdnames the occurrence: a resent id is a duplicate. Lowercase letters, digits, ., _, @, -, at most 128, not starting awf.a new id
sourcethe integration that sent it; the event a wait returns carries itnone

It resolves once the run has the event: when it starts or continues a run, after the attempt has started, which can take seconds. The result is awf emit --json's line:

FieldValues
okwhether the run has the event and needs no one; below
outcomeaccepted, duplicate, refused
didwhat was done to the run: started, nudged, continued, none
whywith none or refused: returned, not-a-start, stuck, stopped, cancelled, nothing-due, nudge-failed, launch-failed, opened
errorwith launch-failed: what the attempt said before it exited
ref, eventIdthe run, {workflow}/{id}, and the event's id

When it is ok ​

ok is false when the event is refused (the run has returned, or there is no run and the event doesn't start one), or the run wasn't woken because its launch failed or the operator must step in:

whyokWhat it means
noneyesstarted, nudged or continued
nothing-dueyesthe run is parked at a wait that doesn't take this event; a later wait will
nudge-failedyesthe running attempt wasn't nudged, but its wait takes the event
openedyesawf open --no-fork has an agent's session open; the run is woken once it exits
returnedyes, if duplicatethe event was already taken before the run returned
returned, not-a-startnorefused: no run will take it
launch-failednothe event is kept; the next event, a resend or awf tick launches again
stuck, stopped, cancellednothe run needs the operator (Runs)

start ​

ts
export async function notes(runs: Client, version: string) {
  const started = await runs.start(release, { version, dryRun: true }, { id: `v${version}` });
  if (!started.ok) throw new Error(`${started.did}: ${started.error ?? ""}`);
  const rest = await started.run.wait({
    final: true,
    signal: AbortSignal.timeout(30 * 60_000),
  });
  return rest.ending?.kind === "completed" ? rest.ending.value : undefined;
}

Makes sure the run with these args exists and has been launched, and resolves once its attempt has started. args is what the workflow's prepare would return, checked by tsc; prepare never runs, and every attempt of the run gets the args the run recorded.

OptionWhat it isDefault
idthe run's idthe workflow's id(args), or else a new id; a workflow with starts and no id(args) throws no-id
okdidWhat it means
yesstarteda new run, launched
yesexistsa run with these args was there; launched only if it never had an attempt
nolaunch-failedthe run is kept; error says what the attempt said. A later start tries again
nostuckits launch failed three times; the operator's turn

Every result carries run, a Run. A run there with other args, or one an event or awf run began, throws other-args.

get ​

ts
const run = await runs.get(review, "pr-7");

The Run another call or another program started. No run with that id throws no-run.

inbox ​

ts
const items = await runs.inbox(review, { after: cursor });

What the workflow's runs posted and asked, oldest first, as awf inbox --workflow --json lists it. Each item carries ref, at and a cursor; pass the last item's cursor as after to read on from just after it. A cursor made for another workflow throws cursor.

kindWhat it is
postparticipant.post(text): participant, text, labels
messagea message between an agent and a participant; asks: true for a question open until an answered or expired item
answereda question's answer, and what it came through
expireda question that closed unanswered
routea route between an agent and a participant opened or closed
endedan attempt ended: its outcome and, when parked, the wait's label

A person answers a question with awf reply; the client has no reply call. A reply event the workflow declares, {key}:reply, goes through emit.

Run ​

A run's handle, read from its files whenever asked: id, ref ({workflow}/{id}, as awf runs and awf logs take it), and two calls.

status() resolves to the run as it is now, a RunStatus<Result>:

FieldWhat it is
stateas awf runs reads it: running, parked, completed, stopped, failed, timed-out, cancelled, interrupted, stuck
finalnothing will continue it on its own
endingthe last attempt's, once one has ended: completed with value, typed as the workflow's result; parked with its wait's label; or unfinished with reason and the continue command

final is true for completed, stopped, cancelled and stuck; for a plain run also failed, timed-out and interrupted. An event run that failed isn't final: its next event or awf tick continues it.

wait(options?) resolves at the run's next rest after the call: it ends, parks, crashes or sticks. It resolves at once if the run isn't running.

OptionWhat it is
finalwait for the first rest that is final instead of the next
signalan AbortSignal; the wait rejects with its reason when it aborts

Bound a final wait with a signal: a failed event run with no next event and no awf tick never becomes final. A wait also rejects when the run's files can't be read.

ClientError ​

ts
export async function closeIfThere(runs: Client, pr: number) {
  try {
    return await runs.emit(review, `pr-${pr}`, "thread:reply", {
      text: "closing",
      author: "bot",
    });
  } catch (error) {
    if (error instanceof ClientError && error.code === "no-run") return undefined;
    throw error;
  }
}

A call the program got wrong throws a ClientError, with a code, a ClientErrorCode, and, when it was about a run, its ref. Anything the run decided (a refused event, a failed launch) is in the result instead.

codeThrown when
eventthe workflow doesn't take the event's name, or its schema refuses the data
ida run or event id the grammar refuses, or one id(args) returns
no-idstart of a workflow with starts, with no id and no id(args)
preparea starting event whose prepare refuses no argv, an id(args) that throws, or args that aren't JSON
filethe workflow's file isn't the file that exports it as its default
projectthe workflow sits directly in your home folder, not in a project
no-enginethe project has no agentswf, or declares one that isn't installed
other-enginethe project's agentswf is another version than the client's (Install)
no-runget, or a {key}:reply, of a run that isn't there
other-argsstart of a run there with other args, or with none recorded
cursoran after made for another run, workflow or filter

An awf command run with --json uses the same word for the same failure.

See also ​

  • Programs: the client in a webhook, a relay and a release script.
  • examples/programs: a program you can run, with its test.
  • Testing API: fakeClient, to test a program without runs.
  • awf: awf emit, awf inbox and awf runs, the same calls from a shell.