AGENTO
v1.1.0 · Documentation
Documentation/Compose workflows/Booking workflow exercise

Booking workflow exercise

2026-10-15·source 4bebed4·6 min read·15 / 27

You will learn: Practice the complete workflow in an isolated optional exercise.

This optional exercise follows mapping, dependencies, and confirmation. Fixed responses let you test expiry, repeated approval, and failure cases without creating real bookings. To connect your own API first, use the real-endpoint quick start.

The scenario is deliberately small: the user supplies a date, the API supplies available slots, the user chooses a time, and the host asks for approval before creating a booking.

The API contract

  • GET /availability?date=2026-10-01 returns { slots: [{ slot_token, label }] }.
  • POST /bookings accepts { date, slot_token, notes? }.
  • The POST receives the user's Bearer token and returns { booking_id, time }.

These endpoints are a fixture, not a public demo service. The script below intercepts requests, so no network booking is made.

1. Save the complete configuration

Download config.yml. This is a complete file; no fragments from other pages are required.

version: "2"
assistant:
  language: auto
  timezone: Asia/Hebron
tools:
  - id: availability
    tool:
      description: Find available appointment times for a date. This tool does not create a booking.
      input_schema:
        type: object
        properties:
          date: { type: string, format: date }
        required: [date]
    request:
      method: GET
      url: https://api.example.com/availability
      map:
        query:
          date: $.date
    response:
      private_fields: [slot_token]
      output_schema:
        type: object
        properties:
          slots:
            type: array
            items:
              type: object
              properties:
                slot_token: { type: string }
                label: { type: string }
              required: [slot_token, label]
        required: [slots]

  - id: book
    tool:
      description: Book an appointment for a date after selecting an available time and confirming.
      input_schema:
        type: object
        properties:
          date: { type: string, format: date }
          slot_token: { type: string, minLength: 1 }
          notes: { type: string, maxLength: 500 }
        required: [date, slot_token]
    depends_on:
      - tool: availability
        when: always
        arguments:
          date: $.date
        select:
          items_path: $.slots
          id_path: $.slot_token
          label_path: $.label
          id_sensitive: true
          facts:
            time: $.label
        map:
          $.slot_token: $.slot_token
        ttl_ms: 30000
    behavior:
      effect: side-effect
      confirmation:
        required: true
        ttl_ms: 30000
        preview:
          date: $.date
          time: { source: selection, path: $.time }
          notes: $.notes
      recovery:
        refresh_dependency: availability
        statuses: [409, 410]
    request:
      method: POST
      url: https://api.example.com/bookings
      auth:
        type: session
      retry:
        attempts: 1
      map:
        body:
          date: $.date
          slot_token: $.slot_token
          notes: $.notes

Follow the data:

  1. book.date is user-owned. It is forwarded to availability.date.
  2. availability retrieves real slots. Its output schema checks every slot has a token and label.
  3. select describes how to show those slots. id_sensitive hides their real tokens behind outward-facing IDs.
  4. The dependency map writes the selected token into book.slot_token. It is absent from the model-facing book schema.
  5. The selection fact time supplies a human-readable preview, while the POST body uses the real token.
  6. confirmation.required pauses before the write. auth.type: session forwards the host's token; the API enforces permission.

private_fields removes slot_token from display data. It remains available inside runtime dependency resolution. Privacy projection must not destroy the value needed to execute.

2. Run the host-side fixture

Install the npm package as described in installation, then save this beside config.yml as demo.mjs. Download the script.

import assert from "node:assert/strict";
import { AgentRuntime } from "agento-runtime";

const calls = [];
const runtime = await AgentRuntime.create({
  configPath: "./config.yml",
  presentation: "raw",
  fetch: async (url, options) => {
    const path = new URL(url).pathname;
    calls.push({ path, method: options.method });
    if (path === "/availability") {
      return Response.json({
        slots: [
          { slot_token: "slot-a", label: "10:00" },
          { slot_token: "slot-b", label: "11:00" },
        ],
      });
    }
    assert.equal(path, "/bookings");
    assert.equal(options.method, "POST");
    assert.equal(options.headers.authorization, "Bearer demo-user-token");
    assert.deepEqual(JSON.parse(options.body), {
      date: "2026-10-01", slot_token: "slot-b", notes: "Window seat",
    });
    return Response.json({ booking_id: "booking-42", time: "11:00" });
  },
});
const request = {
  sessionId: "booking-demo",
  auth: { token: "demo-user-token" },
};
try {
  let result = await runtime.invoke({
    ...request, tool: "book",
    arguments: { date: "2026-10-01", notes: "Window seat" },
  });
  assert.equal(result.status, "needs_selection");
  assert.equal(calls.length, 1);
  assert(!result.selection.options.some(option => option.id === "slot-b"));
  console.log("1.", result.status, result.selection.options);

  // Test fixture: simulate the user choosing 11:00.
  const choice = result.selection.options.find(option => option.label === "11:00");
  result = await runtime.select({
    ...request, selectionId: result.selection.id, choice: choice.id,
  });
  assert.equal(result.status, "needs_confirmation");
  assert.equal(calls.length, 1);
  assert.equal(result.confirmation.preview.time, "11:00");
  assert(!JSON.stringify(result.confirmation.preview).includes("slot-b"));
  console.log("2.", result.status, result.confirmation.preview);

  // Test fixture only: a real UI waits for explicit user approval here.
  result = await runtime.confirm({
    ...request, confirmationId: result.confirmation.id,
  });
  assert.equal(result.status, "completed");
  assert.equal(calls.filter(call => call.method === "POST").length, 1);
  console.log("3.", result.status, result.data);
  console.log("HTTP calls:", calls);
} finally {
  runtime.dispose();
}

node demo.mjs

The script simulates a choice and approval for this test only. In your product, the choice handler and confirmation handler must wait for the respective user actions.

3. Read the trace

StepResultHTTP calls so far
Invoke book with date and notesneeds_selection with 10:00 and 11:00One GET; zero POSTs.
Select 11:00needs_confirmation with date, time, and notesStill one GET; zero POSTs.
Confirm the returned IDcompleted with booking-42One GET and one POST.

The fixture asserts the exact POST body, checks that the real slot token is not in the displayed choices or preview, and checks that only one POST occurs.

4. Make one change at a time

Remove the date. Expect needs_input before any GET. The runtime distinguishes a missing user field from a dependency-produced field.

Return one slot. Without an explicit matching preference, it is selected automatically. The first result becomes needs_confirmation; adjust the fixture assertions accordingly. Automatic selection is not automatic approval.

Return no slots. Expect needs_input with a message explaining no availability. Do not render a generic “missing date” form just because the status is needs_input.

Reduce dependency TTL to 1 millisecond. A choice may expire before the next call. Expect INPUT_SELECTION_STALE or CONFIRMATION_STALE, depending on where expiry occurs. Refresh the flow instead of resubmitting an old ID.

Return 409 once from the POST. The configured recovery clears the stale dependency and looks up fresh slots. Handle the new selection or confirmation; it must not silently confirm a replacement. The second POST should happen only after a new user approval.

Change when to missing and supply a token directly. The lookup can be skipped. Use this only for a trusted host/navigation path, not as a way to validate user-provided tokens.

5. Connect your real API

Replace the two URLs and response schema, then remove the injected fetch. Provide real credentials from your authenticated backend. Keep the schema, selection paths, mappings, and confirmation preview aligned with real response fixtures.

Create one long-lived runtime per application process. Use a stable, authorized conversation ID for every continuation. Do not allow clients to select another user's sessionId.

For a multi-instance deployment, supply shared session storage with per-session transaction serialization and a suitable shared rate limiter. If the API supports idempotency, configure it before enabling write retries. See production readiness.

6. Grow into a service booking

The next increment is book → availability → services: availability needs a service ID, so put the service dependency on availability. Add service labels as selection facts for the preview. Add user preferences only after the basic choice flow passes its tests.

Do not add every feature at once. Establish one observable behavior, test it, then add the next policy.