Skip to content

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:

Terminal window
r#'{
run: {|| ^tail -F http.log | lines }
}'# | .append xs.service.log.create

The service will:

  • Execute the provided Nushell expression.
  • Stream pipeline output as log.recv frames. Text pipelines emit one frame per line, ByteStream pipelines send binary chunks.
  • Auto-restart on natural completion (the pipeline drained without error). The service stays running until the user appends xs.service.log.term or it crashes.

Data channels

TopicDirectionDescription
<name>.recvsystem -> streamOutput value emitted by the service
<name>.senduser -> service stdinInput fed to a duplex service

Both live in the user/app namespace, not under xs.. They are app data, not lifecycle.

Configuration Options

OptionTypeDefaultDescription
duplexbooleanfalseEnable sending input to the service’s pipeline via <name>.send
return_optionsrecord,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

OptionTypeDefaultDescription
suffixstring".recv"Suffix appended to the service name for output frames. Include the leading . (e.g., ".output" produces <name>.output)
ttlstring"forever"Time-to-live for output frames
targetstringomittedDefault (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:

Terminal window
r#'{
run: {|| ^tail -F access.log | lines },
return_options: {
suffix: ".line",
ttl: "last:100" # Keep only last 100 log lines
}
}'# | .append xs.service.logs.create

This 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 a service_id identifying 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.

Terminal window
r#'{
run: {||
use my-parser
^tail -F data.log | lines | each {|line| my-parser parse $line }
}
}'# | .append xs.service.log.create

Bi-directional Communication

When duplex is enabled, you can send data into the service’s input pipeline via <name>.send frames:

Terminal window
# Create a websocket connection
r#'{
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.send

When running this service:

  • Lines received from the websocket server are emitted as <name>.recv frames.
  • Content from <name>.send frames 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>.replaced frame is emitted, and a new xs.service.<name>.active follows for the reloaded task.
  • If the new script fails to parse, an xs.service.<name>.invalid frame 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-good create as a fallback.

Lifecycle

Services share the unified lifecycle vocabulary documented in the lifecycle reference. In summary:

User inputRuntime ack on successRuntime ack on failure
xs.service.<name>.createxs.service.<name>.activexs.service.<name>.invalid
xs.service.<name>.termxs.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 retries Continue).
  • 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.