Skip to main content
Version: 0.1.0-alpha.5 (alpha)

Call a capsule from your application

Use an LSF client SDK to call a capsule from Rust, TypeScript, Go, C, Java or C#. In this tutorial, run one example in your chosen language. It calls HTTP and storage capsules, handles an application error, cancels a request and checks whether a configuration change succeeded.

The example starts its own temporary node and stops it when finished. It does not change the node from your first tutorial. Client programs run outside capsules. To write the code that runs inside a capsule, use the capsule authoring guide.

1. Prepare the example​

Use the Linux source checkout and Python environment from the first-node guide. Run the Bash blocks in one terminal, from the repository root. The current native clients connect to a node on the same machine; this example uses numeric loopback addresses. Browser applications use the separate application HTTP interface.

Install the tools for your chosen client language from the toolchain setup. Rust, wit-bindgen and Zig are also needed to build the example capsules that the client calls.

set -euo pipefail
umask 077
export CARGO_TARGET_DIR="$PWD/target"
mkdir -p "$CARGO_TARGET_DIR"
CLIENT_DEMO=$(mktemp -d "$CARGO_TARGET_DIR/client-demo.XXXXXXXX")
cargo build --locked -p latent -p latentd

The new directory keeps this attempt's files separate from earlier attempts. The first build can take several minutes.

2. Build the client in your language​

Select one tab and run its commands. The SDKs come from this checkout.

cargo build --locked -p latent-sdk --example provider_workflow

See the Rust client reference for its API.

3. Prepare the capsules and temporary node inputs​

python3 tools/build_guest_capsules.py --output "$CLIENT_DEMO/guests"
LSF_GUEST_CAPSULES="$CLIENT_DEMO/guests" \
LSF_PHASE3_WORKFLOW_FIXTURE_ROOT="$CLIENT_DEMO/inputs" \
cargo test --locked -p latentd --test phase3_workflow_fixture -- \
export_signed_provider_workflow_fixtures --exact --ignored --nocapture

Expect 1 passed; 0 failed. This prepares signed example packages and the temporary node's trust settings. Run the next step soon afterward because the demonstration signatures expire. These credentials are for this temporary example; use package delivery when configuring your own installed node.

4. Run your client​

Define this helper once. It starts the temporary node, runs the selected client and saves the result:

run_client() {
local language=$1
shift
python3 tools/run_sdk_provider_workflow.py \
--cli "$CARGO_TARGET_DIR/debug/latent" --node "$CARGO_TARGET_DIR/debug/latentd" \
--fixture-root "$CLIENT_DEMO/inputs" --language "$language" \
-- "$@" > "$CLIENT_DEMO/result.json"
}

Select the same language you built in step 2:

run_client rust "$CARGO_TARGET_DIR/debug/examples/provider_workflow"

Show what happened:

python3 - "$CLIENT_DEMO/result.json" <<'PY'
import json, sys
with open(sys.argv[1], encoding="utf-8") as source:
result = json.load(source)
checks = result["participant"]["assertions"]
print("Client language:", result["language"])
print("HTTP capsule succeeded:", checks["httpGuest"])
print("Storage capsule succeeded:", checks["blobGuest"])
print("Cancellation completed:", checks["explicitCancellation"])
print("Configuration change recovered:", checks["mutationReceipt"])
print("Node stopped cleanly:", result["nodeShutdown"]["record"]["clean"])
PY

Expect your chosen language followed by five True results. The example also tries intentional failures, such as a wrong credential and an application error. Those cases are part of the demonstration. A command that exits with an error has not completed the walkthrough.

Invoke an admitted guest​

Now follow the code behind the example. Choose a language tab below; the source link opens the complete program with its imports and setup.

Invoke the authorized HTTP and blob guestsChoose a language
Rust
for (provider, expected, assertion) in [("http", 2201, "httpGuest"), ("blob", 4, "blobGuest")] {
let response = client
.invoke(config.request(provider, provider, None)?, options())
.await
.map_err(|_| {
if provider == "http" {
"http-invocation-rpc"
} else {
"blob-invocation-rpc"
}
})?;
let actual = guest(&response.value)?;
if actual != expected {
return Err(match (provider, actual) {
("http", 10) => "http-guest-permission-denied",
("http", 11) => "http-guest-outcome-uncertain",
("http", _) => "http-guest-unexpected-result",
_ => "blob-guest-unexpected-result",
});
}
activations.push(format!("rust-{provider}"));
assertions.insert(assertion, true);
}

The client sends the target capsule, function arguments, a time/resource budget and an activation ID. The HTTP capsule makes its permitted request through the node. The storage capsule writes and reads four bytes. Provider credentials stay with the node; the client does not need them.

A successful call, an error declared by the capsule, and a platform failure are different outcomes. Handle each explicitly. Keep the activation ID if the reply is lost so that you can ask about the original call.

Cancel the wait, then inspect the original identity​

Stopping a client-side wait does not prove that the capsule has stopped. This example retains the activation ID, requests cancellation and checks its status:

Recover an interrupted invocation using its original identityChoose a language
Rust
drop(pending);
let response = client
.cancel(
CancelRequest {
activation_id: identity.clone(),
reason: "explicit recovery".into(),
},
options(),
)
.await
.map_err(|_| "recovery-cancel-rpc")?;
require(
matches!(
response.value.disposition,
CancelDisposition::ACCEPTED | CancelDisposition::ALREADY_TERMINAL
),
"recovery-cancel-disposition",
)?;

Use a fresh, live context or cancellation token for that follow-up. Do not send the invocation again just to find out whether the first one finished. When your program finishes, close its client and await any required shutdown.

Inspect pages and reconcile a mutation​

A management operation has its own operation ID. Keep that ID and the original request so that a lost reply can be resolved without accidentally creating a second change:

Look up the original policy operation receiptChoose a language
Rust
let known = client
.get_policy_operation(
GetPolicyOperationRequest {
operation_id: operation_id.clone(),
},
options(),
)
.await
.map_err(|_| "policy-operation-rpc")?;
require(
known.value.receipt.as_ref() == Some(&receipt),
"operation-recovery-receipt",
)?;

The example reads one result page at a time and follows its continuation token. It also demonstrates a conflicting change. Check the current state before deciding to submit a new operation.

Finish or troubleshoot​

The runner stops its temporary node and local HTTP server automatically. Your builds and result remain under the directory printed by printf '%s\n' "$CLIENT_DEMO". You can remove that directory when you no longer need it.

What you seeWhat to do
Missing compiler, generator or dependencyRevisit the setup link for your selected language and the common capsule toolchain
Expired demonstration signatureStart again at step 1 with a fresh demo directory
Connection or authentication failureUse the runner's temporary configuration and keep its credential file private
An example command exits with an errorKeep its diagnostic and result files; start the next attempt in a fresh demo directory

Continue with provider failure and recovery or policy reconciliation. For detailed limits and language-specific ownership rules, see the shared client API reference.