fleetlessfleetlessdocs
Reference/SDK/Actions, services, publishers & jobs

SDK · Actions, services, publishers & jobs

Starting and cancelling actions, calling services, publishing to a topic, and following jobs — all correlated over the realtime channel.

Generated from the SDK’s own source. The SDK Reference is the narrative; this page is every member, in the order the SDK declares them.

ActionsApi

Long-running work on a robot, reachable as client.actions. An action is a ROS action the developer exposed under a slug: it is invoked, runs as long as it runs, and reports back while it does.

invoke()

ts
invoke(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<{ id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null }>

Invokes an action. Resolves as soon as the job is created — the job id is informative, not the result. Feedback, progress and the eventual result arrive separately over subscribe. A second invoke of the same slug while one is already running is refused busy, with error.details.running naming the running job.

options.patienceMs bounds goal acceptance only — once a goal is accepted this call has already resolved; the job then runs as long as it runs, observed via subscribe, never awaited. options.timeoutMs (this SDK’s own local wait for the acceptance reply) is derived from patienceMs when left unset, and the combination timeoutMs < patienceMs is refused with invalid_option rather than raced.

Parameter Type Required Description
robotId string yes
slug string yes
params Record<string, unknown> yes
options InvokeOptions no

Returns Promise<{ id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null }>.

References: InvokeOptions

cancel()

ts
cancel(robotId: string, slug: string, jobId?: string | null, options?: SendCommandOptions): Promise<{ id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null } | null>

Cancels a job — a real ROS goal cancel on the robot, not a local forget. Resolves with the Job the cancel was actually sent to, or null if nothing matched.

Two different requests, both legitimate:

  • cancel(robotId, slug) — no jobId — is the operator’s stop button: whatever is running on this slug, stop it.
  • cancel(robotId, slug, jobId) cancels that job specifically. If it is not the one running, the platform answers not_found — this never silently falls back to stopping whatever is running, because a caller who named an id has already ruled that out. The failure this closes: a cancel arriving just after its own job ended used to stop the next caller’s job on the same slug.

Read the returned job either way: “I stopped the one I meant”, “there was nothing there”, and “I stopped a job that started after I last looked” are three different outcomes a discarded result cannot tell apart.

Resolving means the robot’s action server accepted the cancel, not that the goal ended. The returned job is usually still running; how it ends arrives as its own update (subscribe). The platform answers from the action server’s CancelGoal return codes, so a cancel can also reject with:

  • cancel_rejected — the server refused (ERROR_REJECTED) and the goal keeps running unless its job later says otherwise. cancelRejectedDetails.parse(error.details).goals lists every goal the cancel reached as { job_id, goal_id, return_code }; compare return_code against CANCEL_RETURN_CODES: none (0, accepted), rejected (1), unknown_goal_id (2), goal_terminated (3, already ended), or null when that goal’s server did not answer.
  • bridge_timeout — the robot’s bridge did not answer in time; whether the cancel reached the server is unknown.
  • the bridge’s own code when it could not ask at all (e.g. unknown_slug, action_server_lost), not_cancellable for a service, and robot_offline.

Cancelling an unknown job cancels every external goal on its action, never another of the platform’s own jobs.

Parameter Type Required Description
robotId string yes
slug string yes
jobId string | null no
options SendCommandOptions no

Returns Promise<{ id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null } | null>.

References: SendCommandOptions

subscribe()

ts
subscribe(robotId: string, slug: string, handlers: JobSubscriptionHandlers): JobSubscription

Subscribes to the slug’s job: state, feedback, progress and result, as they happen. State is observed by slug, not by job id — this is what makes late delivery after a reconnect and a second observer watching the same job both work without special-casing either. Naming a job to cancel does not change this: a slug is still a place a job may be running, not the job itself, and it is still what subscribe watches.

Parameter Type Required Description
robotId string yes
slug string yes
handlers JobSubscriptionHandlers yes

Returns JobSubscription.

References: JobSubscriptionHandlers, JobSubscription

ServicesApi

Request/response calls to a robot, reachable as client.services. A service answers once and is done — one method, nothing to subscribe to.

call()

ts
call(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<unknown>

Calls a service and resolves with its result. A service call is a job underneath — the same job_id exchange and disconnect survival as an action — but that is deliberately invisible here: the caller gets a plain Promise<result>, matching the REST serviceCallResponse shape. There is nothing to subscribe to for a service — no feedback, no progress, no cancel — so this call already waits for the terminal state internally.

options.patienceMs bounds the whole wait for a service call — unlike an action, where it bounds acceptance only — because a service has no further state to observe once it settles; the platform gives up on the ROS call itself after this long.

options.timeoutMs bounds this SDK’s own local wait for the WHOLE call — the ack that a job was created, plus however much of the budget is left for it to then reach a terminal state — not two separate timeoutMs-length windows back to back. A caller who sets timeoutMs: 5000 bounds total latency at ~5s, not ~10s.

It is also not independent of patienceMs: left unset, it is derived from patienceMs so this SDK’s local clock cannot fire before the platform’s own deadline has even been reached. Setting both, with timeoutMs shorter than patienceMs, rejects with invalid_option before any request is sent rather than letting the two race — see InvokeOptions.patienceMs for the full reasoning.

Parameter Type Required Description
robotId string yes
slug string yes
params Record<string, unknown> yes
options InvokeOptions no

Returns Promise<unknown>.

References: InvokeOptions

PublishersApi

One-way messages to a robot’s publishers, reachable as client.publishers — a velocity command, a goal pose, anything the developer exposed as a publisher.

publish()

ts
publish(robotId: string, slug: string, message: Record<string, unknown>, options?: SendCommandOptions): Promise<void>

Publishes one message to a publisher.

A plain method call — deliberately no deadman switch, rate governor or “takt” helper. The bridge’s own timeout_ms failsafe is the platform’s safety primitive: when messages stop arriving, crash included, the bridge publishes its configured failsafe message. That does not cover a caller who stops calling publish on purpose without stopping cleanly (e.g. no repeated call at a safe rate) — how often and when to publish is the app’s pattern, not the SDK’s. See the Publishers section of the SDK reference before building a publisher-driven control loop.

Rejects publisher_busy while a different user is publishing and has not been quiet for its configured quiet timeout yet — whoever publishes holds the publisher implicitly exclusive.

Parameter Type Required Description
robotId string yes
slug string yes
message Record<string, unknown> yes
options SendCommandOptions no

Returns Promise<void>.

References: SendCommandOptions

JobsApi

Robot-wide job reads, reachable as client.jobs — addressed by robot, not slug, which actions and services cannot do.

list()

ts
list(robotId: string): Promise<({ id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null })[]>

Every job the platform currently believes this robot has.

actions.subscribe/services.call and GET /jobs/:slug — the per-slug route those build on — all require knowing the slug already. Two cases don’t: a reconnecting bridge naming a job the cloud only adopted, and a config change leaving a job on a slug the published document no longer contains. Neither has a slug to give — this method is the only way an app developer reaches them.

At most one entry per slug: the current job there, same as a per-slug read would answer. Not a history endpoint — that is history below. Grant-filtered same as cameras.list/datapoints — an end user or server key sees only jobs on slugs their role grants, a developer session sees every job on the robot. Never empty-vs-missing ambiguity: a robot doing nothing resolves [].

Ordered newest first by started_at, with job.seq as the tiebreaker (started_at alone is not a total order — two jobs minted in the same millisecond used to sort arbitrarily, differently on each query). But for an adopted job, started_at is adoption time, not when it actually started on the robot — the cloud only learns of it at hello, having never minted it, and has no other honest value to put there. So this is newest-known-first: a job the robot has been running for an hour can sit above one started a minute ago, if the hour-long one was only just adopted.

Parameter Type Required Description
robotId string yes

Returns Promise<({ id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null })[]>.

history()

ts
history(robotId: string, options?: JobHistoryOptions): Promise<{ runs: ({ id: string; robot_id: string; slug: string; kind: "action" | "service"; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; started_at: string; ended_at: string | null; duration_ms: number | null; result: unknown; error: { code: string; message: string; details?: unknown } | null; actor: { kind: "developer" | "server_key" | "end_user" | "app_user"; id: string; label: string }; seq: number; progress: number | null; feedback: unknown })[]; next_cursor: number | null }>

What has run on this robot: one row per run, newest first by the durable seq, with its actor, its outcome and its duration_ms, kept for 90 days. The durable counterpart of list.

Needs the action_history capability on the caller’s role, or it rejects capability_required. An app user sees only runs on the slugs their role grants; a developer session sees the whole robot.

Page until next_cursor is null, never until a page looks short. The cloud applies the role’s grants to the page it already read, so a page can come back thin — or empty — with a perfectly good non-null cursor behind it. runs.length === 0 is not an end-of-data signal.

It lags realtime by a moment, deliberately: a run watched to completion over actions.subscribe can still read running here for an instant. Render the outcome from the realtime job you already have; use this for what you were not watching.

Parameter Type Required Description
robotId string yes
options JobHistoryOptions no

Returns Promise<{ runs: ({ id: string; robot_id: string; slug: string; kind: "action" | "service"; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; started_at: string; ended_at: string | null; duration_ms: number | null; result: unknown; error: { code: string; message: string; details?: unknown } | null; actor: { kind: "developer" | "server_key" | "end_user" | "app_user"; id: string; label: string }; seq: number; progress: number | null; feedback: unknown })[]; next_cursor: number | null }>.

References: JobHistoryOptions

JobHistoryOptions

The filters jobs.history reads. Every field is optional; the wire names are snake_case and this SDK spells them the way its other options are spelt. fromMs/toMs are unix milliseconds, a half-open window [from, to) so adjacent windows never both contain the run on their boundary. limit above the platform’s page cap is refused with validation_error, not quietly reduced.

Property Type Required Description
slug string no Only runs of this action or service.
state "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost" no Only runs in this state: running, unknown, succeeded, failed, cancelled or lost.
kind "action" | "service" no Only action runs, or only service runs.
limit number no How many runs to return, 1 to 200; absent means 100. Above the cap the platform refuses with validation_error rather than trimming.
beforeSeq number no The previous page’s next_cursor. Send it back rather than computing one.
fromMs number no Only runs that started at or after this unix timestamp in milliseconds; with toMs the window is half-open, [from, to).
toMs number no Only runs that started before this unix timestamp in milliseconds.

JobRun

The job-run wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

JobRunListResponse

The job-run-list-response wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

JobSubscription

A live job subscription, returned by actions.subscribe.

unsubscribe()

ts
unsubscribe(): void

Stops this subscription. The unsubscribe frame reaches the server only when this was the last holder of the robot/slug pair and the channel is connected — subscriptions are reference-counted across kinds, so a second actions.subscribe, datapoints.subscribe or in-flight services.call on the same pair keeps its stream running. Safe to call more than once.

Returns void.

JobSubscriptionHandlers

The callbacks actions.subscribe reports through: one for every job update on the slug, one for a refusal of the subscription itself.

onJob()

ts
onJob(event: { type: "job"; robot_id: string; slug: string; job: { id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null }; feedback: unknown; progress: number | null; timestamp_ms: number }): void

Called on every update pushed for the slug’s current job — state, feedback, progress and result.

Parameter Type Required Description
event { type: "job"; robot_id: string; slug: string; job: { id: string; robot_id: string; slug: string; state: "failed" | "succeeded" | "unknown" | "running" | "cancelled" | "lost"; origin: "external" | "fleetless"; started_at: string; updated_at: string; seq: number; result: unknown; error: { code: string; message: string; details?: unknown } | null }; feedback: unknown; progress: number | null; timestamp_ms: number } yes

Returns void.

onError()

ts
onError?(error: FleetlessError): void

Optional — a caller need not implement it.

Called once if the subscription is refused, e.g. forbidden or an unknown slug.

Parameter Type Required Description
error FleetlessError yes

Returns void.

References: FleetlessError

SendCommandOptions

The options every realtime command accepts — actions.cancel and publishers.publish take exactly these; InvokeOptions extends them for actions.invoke and services.call.

Property Type Required Description
timeoutMs number no How long to wait for a command_result before rejecting command_timeout. Default 10s.

InvokeOptions

What actions.invoke and services.call accept on top of SendCommandOptions: the two clocks a command runs under, one local to this SDK and one on the platform.

Property Type Required Description
patienceMs number no How long the platform itself should wait for this one call before giving up on the robot — the whole wait for a service call, goal acceptance only for an action (once accepted, a job runs as long as it runs and is observed, not awaited). Optional; absent means the platform’s own DEFAULT_PATIENCE_MS, 15s — exactly the behaviour of a caller who names no preference. Outside MIN_PATIENCE_MS to MAX_PATIENCE_MS (1s to 120s, both exported by @fleetless/contracts) the platform refuses with validation_error rather than clamping, and this SDK does not clamp locally or retry: surfacing the refusal is the whole of what it does with this field. The floor exists because impatience reaches the robot, not just the platform — a patience too short to survive a goal-acceptance round trip made the bridge report goal_timeout and then issue a corrective cancel against a goal an action server accepted a moment later, so a caller who names an unreachable deadline was causing a real cancellation on the machine, repeatably, not just receiving an error. timeoutMs bounds how long this SDK waits locally for a reply on the wire it already sent on; patienceMs travels to the platform and bounds what it is willing to wait for from the robot. They are not set independently of each other. timeoutMs left unset is derived from patienceMs, not defaulted to a fixed number that might be shorter — the SDK giving up locally before the platform’s own deadline would report command_timeout for a call the platform never actually refused. Setting both explicitly with timeoutMs < patienceMs is refused with invalid_option before any request is sent, for the same reason.

Inherited from SendCommandOptions: timeoutMs.

References: SendCommandOptions

Job

The job wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

JobState

The job-state wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

JobOrigin

Who started a job: fleetless for every job the cloud minted from an invocation, external for a goal the bridge found active on a published action without having sent it. An external job has no parameters and no starter (ROS 2 publishes neither) and is never in jobs.history. The type of Job.origin, which every job carries.

The wire shape is contracts’ jobOrigin; the alias exists so the reference can describe it — a JSDoc on an export type { … } from statement does not survive bundling.

ts
type JobOrigin = JobOrigin$1

JobEvent

The job-event wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.