pulse-client — Rust SDK for StreamFlow Pulse
Official Rust client for Pulse — the AI Agent Platform. Async-first, reqwest + serde stack, MSRV 1.82.
use PulseClient;
async
Install
[]
= "2.6.0"
= { = "1", = ["macros", "rt-multi-thread"] }
Requires Rust 1.82+ as a best-effort MSRV (declared in Cargo.toml). CI tests against stable only — the transitive dep graph (reqwest → hyper-util → tokio-rustls → base64ct → …) shifts its own floor frequently, so chasing an MSRV in CI produces flaky red builds for reasons unrelated to this code. If you hit a build error on a Rust older than stable, bump your toolchain.
Why pulse-client (Rust)
- Async-first — every method returns
Future. Drops naturally into tokio + axum + actix. - Three external deps —
reqwest(HTTP, the de facto standard) +serde+serde_json. No Hyper-direct fiddling, no custom transports. rustlsby default — no system OpenSSL dance. Cross-compile works out of the box.- Sibling parity — same surface + naming as the Python (
pulse-py), JavaScript (@olsisoft/pulse-client), Java (com.streamflow:pulse-client), and Go (github.com/olsisoft/pulse-go) SDKs. - Cheap to clone —
PulseClient: Clone, the underlyingreqwest::Clientpools connections, the token sits behindArc<RwLock>. Share a single instance across tasks. - Spec-aligned — every method corresponds 1:1 to an endpoint in the Pulse OpenAPI 3.1 spec. Drift caught at PR time by the in-tree spec invariant tests (B-103).
Quick start
use Duration;
use ;
async
Supported surfaces (v2.7.x)
| Resource | Methods | Notes |
|---|---|---|
client.auth() |
login(user, pass), refresh(refresh_token), organizations(), switch_org(org_id) |
Auto-caches JWT after login / refresh / switch_org. |
client.pipelines() |
list(), get(id), create(definition), delete(id) |
definition follows the CreatePipelineRequest schema. |
client.agents() |
list(), get(id) |
Read-only — agents are owned by pipelines. |
client.templates() |
list() |
The 223+ first-party templates. |
client.users() |
list() |
Requires USERS_LIST permission (Owner / Platform Admin personas). |
client.version() |
top-level | Public — no JWT required. |
Every method returns impl Future<Output = Result<Value, PulseError>>. Value is the re-exported serde_json::Value — full document, no schema-bound DTOs (yet). Schema-bound types land in v3.0.
Full ~112-endpoint surface documented in Swagger UI at <pulse-server>/api-docs. Less-used methods land opportunistically as user-facing demand surfaces.
Embedded ML inference & duplex
Score events with an uploaded ONNX model in-process (B-112), and open a bidirectional duplex channel for synchronous decisions (B-114). Full guide: ML inference & duplex.
use ;
use BTreeMap;
// Upload + score with an ONNX model (no model-server hop)
let schema = from;
client.models.upload.await?;
builder.from_topic
.ml_predict
.filter.to_topic;
// Duplex: one connection, send in / receive the correlated output
let mut ch = client.duplex.await?;
let cid = ch.send.await?;
let out = ch.recv.await?; // out.correlation_id == Some("tx-1")
ch.close.await?;
Sandboxed WASM transforms
Run an uploaded WebAssembly module over each event (B-110), sandboxed in pure-Java
Chicory on the engine — no host syscalls, bounded linear memory. Any wasm32
toolchain (Rust, TinyGo, AssemblyScript, C) can author a module against the
alloc/process ABI; upload / delete require the ADMIN role.
use ;
// Upload a module, then run it inline in a stream
client.wasm.upload.await?;
client.streams.deploy.await?;
Legacy formats & protocols — the headline use case. Compile any existing
parser to wasm32 and drop it in as a single-message transform to bring legacy
data into the pipeline — COBOL copybooks, FIX, HL7, EDI X12, ASN.1, Modbus, …
You don't rewrite the parser, you wrap it (see the pulse-wasm-guest guest SDK for
the Rust/TinyGo/AssemblyScript/C operator ABI). Pair it with ml_predict (ONNX
above) to parse and score each event in-stream, no external service.
Authentication
Where credentials come from
The SDK authenticates as a Pulse user — there are no separate API keys to provision. A username + password (or a JWT minted from them) is all you need, and they live in your own Pulse instance, not on streamflowmesh.io.
- First run → bootstrap admin. The very first account is created either by
the first-run screen of the Pulse web/desktop app, or by a single
unauthenticated
POST /api/auth/registerwith a{"username","password"}body while no user exists yet. That first user is granted ADMIN. As soon as any user exists,/api/auth/registerlocks down and requires an admin JWT — so the open bootstrap can only ever mint the very first account. - Additional users. An admin creates more accounts from Settings → Users
in the Pulse UI (or an admin-authenticated
registercall). Give each CI job or service integration its own dedicated user rather than sharing the admin. - Exchange for a token.
auth().login(user, pass)returns a short-lived access JWT (~1 h TTL) plus a refresh token; the client caches the access token automatically. In CI, either callloginat startup, or pass a pre-minted JWT (pattern 2 below) and refresh it before it expires.
base_url(...) points at your Pulse server — http://localhost:9090 for a
local pulse --headless or desktop install, or your deployed Pulse URL.
Passing the token to the client
Three patterns:
// 1. Username + password (interactive / CLI tools)
let client = builder
.base_url
.build?;
client.auth.login.await?;
// 2. Pre-minted JWT (CI / service accounts)
let client = builder
.base_url
.token
.build?;
// 3. Hot token rotation (long-running daemons)
client.set_token;
client.clear_token; // log out
For long-running processes, persist refreshToken from login() and call client.auth().refresh(&refresh_token) before the JWT expires (default 1 h TTL).
Error handling
use PulseError;
match client.pipelines.get.await
Convenience predicates: err.is_auth_error(), is_not_found(), is_validation_error(), is_rate_limited(). Every error carries status_code(), path(), body().
Custom reqwest::Client (proxies, mTLS, shared pools, tracing)
let shared = builder
.timeout
.proxy
// .add_root_certificate(...) // for mTLS / internal CAs
.build?;
let client = builder
.base_url
.http_client
.build?;
Development
CI runs the same on every push touching pulse-rs/ — see .github/workflows/pulse-rs.yaml.
Automatic retry (opt-in)
Off by default — one attempt per request. Enable bounded, full-jitter
exponential-backoff retries via RetryPolicy:
use RetryPolicy;
let client = builder
.base_url
.retry // or a full RetryPolicy { .. }
.build?;
429 (rate limited) is retried for any method, honouring Retry-After;
on_status 5xx (default 502/503/504) and transport errors are retried only for
idempotent methods (GET/HEAD/PUT/DELETE) unless retry_non_idempotent; terminal
4xx are never retried.
Local pipeline simulation (Python-only today)
The streams DSL is client-side declaration, server-side execution:
streams().compile(&builder) builds the pipeline JSON locally (no network) and
streams().deploy(&builder) runs it on the Pulse engine. This SDK has no
in-process simulator — to validate a pipeline before deploy, compile() and
inspect the JSON, or deploy to a dev Pulse.
A local
TopologyTestDriver-style executor that runs a streams pipeline in-process over sample events (StreamBuilder::simulate(events)) currently exists only in the Python SDK (streamflow-pulse-client). Cross-language parity is tracked as B-169 (issue #311); until then, local simulation is a Python-exclusive capability.
Roadmap
- v2.5.x — current async API, 5 core resources,
version(). - v2.6.x — expanded resource coverage: backups, schedules, credentials, settings, approvals, chat.
- v3.0 — schema-bound DTOs (typed structs instead of
serde_json::Value); event-stream consumer as aStream<Item = Event>consuming/api/pulse/events/stream(SSE). - B-098 satellite — once
olsisoft/pulse-rsexists, this in-tree code lifts out and publishes to crates.io.cargo add pulse-clientwill switch to the satellite; in-tree continues to mirror for one release cycle.
Track progress in docs/STREAMFLOW-BACKLOG.md under item B-098.
License
Apache 2.0 — same as the parent Pulse repository.