Services
cross.stream services use Nushell closures to create streams of data
that are emitted as frames into the store.
You can also compose external request-response servers, particularly CLI tools that read requests on stdin and write responses to stdout, using duplex services.
Basic Usage
To create a service, append a Nushell script that evaluates to a configuration
record with a run closure using the topic xs.service.<name>.create:
r#'{ run: {|| ^tail -F http.log | lines }}'# | .append xs.service.log.createThe service will:
- Execute the provided Nushell expression.
- Stream pipeline output as
log.recvframes. Text pipelines emit one frame per line,ByteStreampipelines send binary chunks. - Auto-restart on natural completion (the pipeline drained without error). The
service stays running until the user appends
xs.service.log.termor it crashes.
Data channels
| Topic | Direction | Description |
|---|---|---|
<name>.recv | system -> stream | Output value emitted by the service |
<name>.send | user -> service stdin | Input fed to a duplex service |
Both live in the user/app namespace, not under xs.. They are app data, not
lifecycle.
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
duplex | boolean | false | Enable sending input to the service’s pipeline via <name>.send |
return_options | record | , | Customize output frames (see Return Options) |
The return_options field controls the suffix and TTL for the .recv frames
produced by the service.
Return Options
| Option | Type | Default | Description |
|---|---|---|---|
suffix | string | ".recv" | Suffix appended to the service name for output frames. Include the leading . (e.g., ".output" produces <name>.output) |
ttl | string | "forever" | Time-to-live for output frames |
target | string | omitted | Default (omitted): output must be a record, stored as frame metadata. Set to "cas" to store in CAS; any type is accepted. |
TTL Options
"forever", Frames never expire (default)"ephemeral", Frames are removed immediately after processing"time:<ms>", Frames expire after specified milliseconds"last:<n>", Keep only the N most recent frames
Example with custom return options:
r#'{ run: {|| ^tail -F access.log | lines }, return_options: { suffix: ".line", ttl: "last:100" # Keep only last 100 log lines }}'# | .append xs.service.logs.createThis service emits frames with the topic logs.line and automatically
maintains only the 100 most recent log entries.
Built-in Store Commands
Inside the run closure a service has the same store helper commands as actions
and actors:
.append, append a new frame. Metadata you provide is merged with aservice_ididentifying the service that wrote it..cat, read frames from the store (streaming, supports--follow)..last, fetch the most recent frame(s), optionally filtered by topic..cas, read content from CAS by hash..cas-post, write content to CAS and return its hash..get, retrieve a frame by ID..remove, delete a frame from the stream..import, insert a frame verbatim, preserving its ID.
Modules
Services can use modules registered via xs.module.<name> topics. A service
sees the modules as they existed when it was created. See
Module Topics for details.
r#'{ run: {|| use my-parser ^tail -F data.log | lines | each {|line| my-parser parse $line } }}'# | .append xs.service.log.createBi-directional Communication
When duplex is enabled, you can send data into the service’s input pipeline
via <name>.send frames:
# Create a websocket connectionr#'{ run: {|| websocat wss://echo.websocket.org | lines }, duplex: true}'# | .append xs.service.echo.create
# Send input to the websocket: note the "\n", wss://echo.websocket.org won't# reply until it sees a complete line."hello\n" | .append echo.sendWhen running this service:
- Lines received from the websocket server are emitted as
<name>.recvframes. - Content from
<name>.sendframes is sent to the websocket server.
Auto-restart and hot-reload
When the script’s pipeline runs to completion without error, the service waits one second and restarts the pipeline. No lifecycle frame is emitted for this internal restart, the service is still considered “running.”
Appending a new xs.service.<name>.create frame while the service is running
reloads it with the new script:
- If the new script parses, the old task is killed, an
xs.service.<name>.replacedframe is emitted, and a newxs.service.<name>.activefollows for the reloaded task. - If the new script fails to parse, an
xs.service.<name>.invalidframe is emitted and the previous service keeps running. The next restart of xs also keeps the previous (good) version, because compaction holds the last known-goodcreateas a fallback.
Lifecycle
Services share the unified lifecycle vocabulary documented in the lifecycle reference. In summary:
| User input | Runtime ack on success | Runtime ack on failure |
|---|---|---|
xs.service.<name>.create | xs.service.<name>.active | xs.service.<name>.invalid |
xs.service.<name>.term | xs.service.<name>.fin.term | , |
Plus the runtime emits:
xs.service.<name>.fin.ok, the run loop exited because the script returned with no auto-restart left to do (rare; today the loop always retriesContinue).xs.service.<name>.fin.error, runtime crash in the script.xs.service.<name>.replaced, hot-reloaded by a newer.create.xs.service.<name>.stopped, xs is shutting down (xs.stopping). The service was alive; it will resume on the next start.
See the lifecycle reference for the full event list, the compaction algorithm, and the invariants the dispatcher honors.