End-to-End Checkout Journey
This example models a realistic e-commerce user journey as a single LPS plan:
browse the catalog -> add items to a cart -> sign in at checkout -> place the order
It is a stateful flow. Each step reads data produced by the previous one (product ids, a cart id, and an auth token) and carries it forward. Because every virtual client runs the whole journey independently, the captured data is session-scoped so clients never step on each other's cart or token.
It runs against fakestoreapi.com (a public mock store API), so you can execute it as-is. See the caveats section at the end.
What this example demonstrates
- Top-level
variables(global defaults) andenvironmentsoverrides for load and quantity knobs. - Capturing responses as navigable JSON (
capture: { to, as: Json }) and reading paths with${name.Body.path}. beforeandafterhooks to prepare data between steps.- Declarative methods:
$find,$counter,$uuid,$timestamp, and$set(including arithmetic). - Guarding steps with
skipIf. - Capturing and reusing an auth token across requests (
Authorization: Bearer ${auth.Body.token}). - Retry driven by this request's captured response (
${order.StatusCode}). - Termination and failure rules on the iteration.
- Variable scope (session vs. global) in practice.
The plan
name: ShopCheckoutJourney
variables:
- name: vuCount # global variable: number of virtual clients
value: 10
- name: arrivalDelay # global variable: client start stagger (ms)
value: 400
- name: firstItemQty # global variable: quantity for first selected item
value: 1
- name: secondItemQty # global variable: quantity for second selected item
value: 2
environments:
- name: development
variables:
- name: vuCount
value: 1
- name: arrivalDelay
value: 400
- name: firstItemQty
value: 1
- name: secondItemQty
value: 2
- name: staging
variables:
- name: vuCount
value: 25
- name: arrivalDelay
value: 250
- name: firstItemQty
value: 2
- name: secondItemQty
value: 3
rounds:
- name: CheckoutRound
numberOfClients: ${vuCount} # each client runs the whole journey. If omitted, defaults to 1.
arrivalDelay: ${arrivalDelay} # stagger client start by 400ms
runInParallel: false # false is the default, so you can omit it; iterations run in order per client (browse -> cart -> order)
iterations:
# ---------- STEP 1: browse the catalog ----------
- name: browseProducts
requestCount: 1 # 1 is the default, so you can omit it; number of requests this client makes for this step (iteration)
httpRequest:
url: https://fakestoreapi.com/products
method: GET
capture:
to: products # session-scoped (makeGlobal defaults to false)
as: Json # ${products.Body} is now navigable JSON
after:
# pick the two cheapest in-stock items to buy, and remember how many were affordable
- '$find(source=$products.Body, where=item.price < 50, select=item.id, match=first, variable=firstItemId)'
- '$find(source=$products.Body, where=item.price < 50, select=item.id, match=index:1, variable=secondItemId)'
- '$find(source=$products.Body, where=item.price < 50, match=count, variable=affordableCount)'
# a per-session idempotency key for the order we are about to build
- '$uuid(prefix=order-, variable=orderKey)'
# a per-session user id from a session-scoped counter (stores straight into ${userId})
- '$counter(start=1, reset=1000, step=1, name=userIdSeq, variable=userId)'
# ---------- STEP 2: add the selected items to the cart ----------
- name: addToCart
requestCount: 1
httpRequest:
# don't try to build a cart if step 1 found nothing to buy
skipIf: '${affordableCount} < 2'
url: https://fakestoreapi.com/carts
method: POST
headers:
Content-Type: application/json
payload:
type: Raw
raw: |
{
"userId": ${userId},
"products": [
{ "productId": ${firstItemId}, "quantity": ${firstItemQty} },
{ "productId": ${secondItemId}, "quantity": ${secondItemQty} }
]
}
capture:
to: cart # session-scoped: this client's cart only
as: Json
after:
# compute total quantity from configurable variables (as=int evaluates the expression)
- '$set(variable=totalQuantity, value=${firstItemQty}+${secondItemQty}, as=int)'
# ---------- STEP 3: checkout - sign in to obtain a bearer token ----------
- name: checkout
requestCount: 1
httpRequest:
# no point authenticating if there is no cart to check out
skipIf: '"${cart.Body.id}" == ""'
url: https://fakestoreapi.com/auth/login
method: POST
headers:
Content-Type: application/json
payload:
type: Raw
raw: |
{
"username": "mor_2314",
"password": "83r5^_"
}
capture:
to: auth # session-scoped: this client's login token
as: Json # token is at ${auth.Body.token}
# ---------- STEP 4: place the order (finalize the cart, authenticated) ----------
- name: placeOrder
requestCount: 1
httpRequest:
# only order if a cart was actually created in step 2
skipIf: '"${cart.Body.id}" == ""'
# fakestoreapi has NO /orders endpoint; "placing the order" here finalizes the cart
# by updating it (PUT /carts/{id}). Swap in your real checkout endpoint for a real test.
url: https://fakestoreapi.com/carts/${cart.Body.id}
method: PUT
headers:
Content-Type: application/json
Idempotency-Key: '${orderKey}'
Authorization: 'Bearer ${auth.Body.token}'
payload:
type: Raw
raw: |
{
"userId": ${userId},
"date": "$timestamp(format=yyyy-MM-dd)",
"totalQuantity": ${totalQuantity},
"products": [
{ "productId": ${firstItemId}, "quantity": ${firstItemQty} },
{ "productId": ${secondItemId}, "quantity": ${secondItemQty} }
]
}
capture:
to: order
as: Json
retry:
# ${order} is THIS request's captured response (there is no built-in ${LastResponse});
# retry only on server errors, never on 2xx/4xx.
if: ${order.StatusCode} >= 500
maxRetries: 3
delayInMs: 500
strategy: Fixed
# guardrails for this step (termination/failure rules live on the iteration, not the round)
terminationRules:
- metric: "ErrorRate > 0.25"
gracePeriod: "00:00:10"
failureRules:
# fail this iteration if the checkout is too slow or too many requests fail
- metric: "TotalTime.P90 > 800" # 90th-percentile latency over 800 ms
- metric: "ErrorRate > 0.05" # more than 5% of requests failed
Running the plan
Run against a specific environment:
lps run fshoppingexample.yaml --environment development
If you do not pass an environment, LPS uses the top-level variable defaults in the file. In this example, that means vuCount: 10 (plus the other top-level values).
How it works, step by step
The top-level variables block defines run-wide defaults, and environments overrides those values per target profile. In this example, vuCount, arrivalDelay, firstItemQty, and secondItemQty can vary by environment (for example, development vs staging) without changing the round body. Note that several properties are optional because they have defaults: numberOfClients -> 1, requestCount -> 1, and runInParallel -> false.
The round runs each of its four iterations in order per client (runInParallel: false), so the journey flows top to bottom for every virtual client.
Step 1: browseProducts
GET /products captures the catalog as products (session-scoped JSON). The after hook then prepares data for later steps:
$find(... match=first ...)and$find(... match=index:1 ...)pick two affordable item ids intofirstItemIdandsecondItemId.$find(... match=count ...)storesaffordableCount.$uuid(prefix=order-, ...)mints an idempotency key inorderKey.$counter(... variable=userId)producesuserId.
Step 2: addToCart
POST /carts builds a cart from values found in step 1.
skipIf: '${affordableCount} < 2'prevents sending a cart request when there are fewer than two matching products.- The response is captured as
cart, so${cart.Body.id}is available downstream. - The
afterhook shows arithmetic from variables via$set(variable=totalQuantity, value=${firstItemQty}+${secondItemQty}, as=int).
Step 3: checkout
POST /auth/login authenticates and captures the response as auth. The token is available at ${auth.Body.token}. A skipIf guard prevents login when no cart exists.
Step 4: placeOrder
The final step updates /carts/${cart.Body.id} to model checkout completion on this mock API.
- URL uses the captured cart id.
- Headers include idempotency key (
${orderKey}) and bearer token (${auth.Body.token}). - Body uses
${userId},${totalQuantity}, selected product ids with${firstItemQty}and${secondItemQty}, and$timestamp(format=yyyy-MM-dd). - Retry is based on
${order.StatusCode}and only retries on server errors (>= 500).
Important: LPS has no built-in ${LastResponse}. You can only reference response data that you explicitly captured (as this example does with order).
Concepts worth calling out
Session scope keeps clients isolated
Captured values and method-stored variables are session-scoped by default. With 25 clients, no one overwrites another client's cart, auth, or userId. If you need shared values, use global scope explicitly.
There is no built-in ${clientId}
LPS does not expose a per-virtual-client id placeholder. Generate per-user values with methods such as $counter, $randomnumber, or $uuid.
Termination and failure rules run at run level
terminationRules and failureRules evaluate aggregate metrics with no session context. If you need a method-produced value in a rule, it must be global (isGlobal=true).
Caveats about the mock API
fakestoreapi.com is a mock and does not persist real commerce state:
POST /cartsalways returnsid: 11.PUT /carts/{id}echoes back the body you send (plusid: 11).POST /auth/loginreturns a token, but that token is not enforced on/carts.
The example still correctly demonstrates placeholder resolution, capture-and-reuse, step chaining, retries, and per-session isolation. For a real checkout test, replace the final PUT /carts/{id} with your real order endpoint.