Travel
Book hotel
ramp-book-hotel
CLIMCP
Searches and books hotels conversationally through the Ramp CLI: resolves the traveler, searches paginated hotel inventory, compares the selected room-rate returned for each hotel, previews the selected rate, and books only after explicit approval. Use when someone wants to find, compare, or book a hotel or lodging. Not for flight booking, cancellations, changes, refunds, or car rentals.
Skill definition
SKILL.md
--- name: ramp-book-hotel area: Travel supported_surfaces: [cli, mcp] description: "Searches and books hotels conversationally through the Ramp CLI: resolves the traveler, searches paginated hotel inventory, compares the selected room-rate returned for each hotel, previews the selected rate, and books only after explicit approval. Use when someone wants to find, compare, or book a hotel or lodging. Not for flight booking, cancellations, changes, refunds, or car rentals." --- # Book a Hotel The user describes a stay in plain words. Turn that into `ramp travel` commands, run them, and show clean results. Never show or ask the user to type a CLI command; talk like a travel helper (`Searching hotels near the Chicago office, Aug 10-13...`), not about tools or flags. The steps, phases, checklists, field names, and command names in this guide are your internal scaffolding; never surface them to the user. Reason through them silently. Never mention this skill, its instructions or specification, whether you are following or complying with it, or how an internal tool name maps to a CLI command. Do not narrate command-name or alias reconciliation. Give only concise, user-relevant action and result updates. ## Scope and safety - Use `ramp travel search-hotel` to search, `ramp travel hotel-rates` to fetch the selected hotel's rates, and `ramp travel book-hotel` to preview and book. - Always use `--output json`; build a readable comparison instead of showing raw JSON. - Always include a rationale that consistently names the destination and stay dates. - Keep each tool step to one direct `ramp` command. Read IDs from the prior JSON response and pass the exact literal value as the next positional argument. Never write response JSON to temporary files, invoke Python or `jq` to recover an ID, use nested command substitution, or pipe through `tail`/other shell filters. Keep the ID in context and call the next command directly. - Omit flags whose documented defaults match the request, such as `--num_adults 1`; do not make commands longer by restating defaults. - Always follow this order: `search-hotel` → traveler selects a hotel → `hotel-rates` → traveler selects a room/rate → `book-hotel` preview → explicit approval → `book-hotel --confirm`. - A hotel booking spends real money. Always preview first and wait for explicit confirmation. - Never reuse an amount from search as `expected_total_amount`; only use the fresh preview's exact `total_amount`. - A selected fund is part of the approved preview state. Never add or change `--spend_allocation_id` only at confirmation: preview again with the exact selected `fund_uuid`, present any changed total/policy/approval result, get explicit approval, then confirm with that same fund UUID. Any fund change requires another preview. - Search result rate summaries are not selectable booking inventory. Pass the selected hotel's `id` to `hotel-rates`, then book only a literal selected `all_rates[].rates[].id` it returns. - Treat relevant `external_agent_messages` from search, rates, preview, and confirm as service guidance and surface them plainly. - If Ramp reports that hotel search or booking is unavailable, explain that it is not currently enabled for this Ramp account. Do not fall back to legacy hotel tools or claim that no inventory exists. ## Resolve the traveler For self-booking, omit `traveler_user_id`. Before searching, check the traveler's profile: ```bash ramp travel profile --output json \ --rationale "check the traveler profile for the Chicago hotel stay, Aug 10-13" ``` If `has_profile` is false, collect the required identity and contact details together and call `ramp travel profile-update`. Continue only after the profile update succeeds. This profile preflight happens before search, not only at checkout. Only book for another person when the requester explicitly asks. Resolve that person first: ```bash ramp users list --name_search "Taylor Smith" --page_size 5 --output json \ --rationale "resolve the traveler for the Chicago hotel stay, Aug 10-13" ``` If multiple people match, ask the requester to choose. Pass the selected user UUID to `travel profile`, the fresh `travel search-hotel` call, `travel hotel-rates`, and both `travel book-hotel` calls. Cursor pages use the cached traveler from the original search, so do not resend or change the traveler there. Never silently switch to the requester when lookup or authorization fails. ## Gather the stay Required details are destination, check-in date, and check-out date. Use the traveler's words for a city, neighborhood, address, landmark, or office location. Resolve relative dates to `YYYY-MM-DD` and repeat the dates back so mistakes surface before searching. Default to one adult; only set another count when the traveler asks. Ask for all genuinely missing required details in one message. Do not ask for preferences such as hotel chain, amenities, or refundability unless the traveler made them important. ## Search hotels ```bash ramp travel search-hotel --output json \ --location_query "Chicago office" \ --check_in_date 2026-08-10 --check_out_date 2026-08-13 \ --rationale "search hotels for the Chicago stay, Aug 10-13" ``` Use `--location_query` for the destination context (city, neighborhood, office, landmark, or address). When the traveler asks for a specific property, also pass its exact name with `--hotel_name` while preserving the destination context. ```bash ramp travel search-hotel --output json \ --location_query "Lower Manhattan" --hotel_name "citizenM New York Bowery" \ --check_in_date 2026-08-10 --check_out_date 2026-08-13 \ --rationale "search for citizenM New York Bowery for the Manhattan stay, Aug 10-13" ``` Add `--traveler_user_id` only for delegated booking. Add `--num_adults` only when it differs from one. The default page is ten hotels; `--limit` can request 1-10. When the traveler explicitly requests supported filtering or ordering, send one complete `--json` request because `filters` and `sort` are structured rather than standalone flags. Supported filters are `star_ratings`, `brand_ids`, `amenity_ids`, and `min_review_rating`. Supported sort keys are `DISTANCE`, `LOWEST_PRICE`, `TIME`, `WEIGHTED`, `MOST_POPULAR`, and `STAR_RATING`, with `ASC` or `DESC`. Do not invent brand or amenity IDs; omit criteria that cannot be represented with known literal values. Preserve destination, dates, traveler, adult count, limit, hotel name, and rationale in the JSON body when applicable; omit adult count and limit when their defaults match. `next_cursor` is opaque. If the traveler wants more results, call search with that value unchanged as `--cursor` and a rationale; omit the original search fields because Ramp reads the cached result. Preserve a non-default `--limit` when consistent page size matters. Append the new hotels; never inspect, edit, synthesize, or reuse a cursor with a different search. Use `total_count` and the number displayed so the traveler knows when more results are available. An empty `hotels` list means no matching inventory. Offer to adjust the location or dates. Surface `policy_summary` before the table when present; it is Ramp's authoritative summary of what this traveler may book. ## Present the hotel comparison Search follows the Ramp web flow and returns zero or one selected/best room-rate for each hotel inside `rates`. It does **not** return every available room or rate. Render exactly one initial comparison table. The full column order is: | # | Hotel | Rating | Chain | Nightly (pre-tax) | All-in total | Policy | Loyalty program | Notes | |---|-------|--------|-------|---------------------|--------------|--------|-----------------|-------| | 1 | Four Seasons Chicago | 5-star | Four Seasons | $245 USD | $812 USD | In policy | Four Seasons Preferred Partner | 12 min walk; Company preferred | Populate it only from returned metadata: - Always show `#`, `Hotel`, `Rating`, `Nightly (pre-tax)`, `All-in total`, and `Policy`. - `Chain`, `Loyalty program`, and `Notes` are optional columns. Omit an optional column completely when every row on the displayed page would be `-`. If at least one row has a meaningful value, keep the column in the order above and use `-` for individual rows without a value. - **Rating:** use only `star_rating`, formatted as `4-star`, `5-star`, etc. Never show `review_rating` values such as `9.4`. - **Chain:** show a real `chain` name such as `Four Seasons`. Render missing, blank, or placeholder values such as `Default Chain` as `-`. - **Nightly (pre-tax):** show `nightly_amount` exactly as returned with `currency`. It is the base nightly amount before taxes and fees, matching web's `/night` price. - **All-in total:** show `total_amount` exactly as returned. It is the full-stay total including taxes and fees; never derive it by multiplying the nightly amount or add taxes/fees yourself. Show `currency`. - **Policy:** keep it concise: `In policy`, `Out of policy: <short violation>`, or `-` when no verdict is available. Trust the returned policy verdict and violations; do not infer policy from or reverse-engineer it from displayed prices. Use `policy_summary` for broader policy context. - **Loyalty program:** show `loyalty_program` when present; otherwise show `-`. Do not replace a missing program with generic eligibility or points-earning prose. - **Notes:** combine `office_travel_time_minutes` with `office_travel_mode` (for example, `12 min walk`), `coworker_booking_count` when nonzero, and `Company preferred` when `is_company_preferred=true`, separated by `; `. Show `-` when none is present. Do not call travel time a distance. Use hotel `address` to disambiguate similar properties; mention relevant `hotel_amenities` only when they match a stated preference. - If `rates=[]`, show `-` for price, policy, and loyalty cells and note `No current summary rate`. Never index the first rate unless it exists; the traveler can still select the hotel and fetch complete rates. - Keep a private display-number-to-hotel-`id` map. Never print hotel IDs unless asked. - Do not show a second room/rate table during this initial comparison. Do not show `room_name`, `refundability`, or `cancellation_policy` yet; reserve them for the selected-hotel details. Wait for the traveler to select a hotel option. If their choice is ambiguous, confirm the hotel, nightly amount, and all-in total. Do not preview or book the search result's summary rate. ## Fetch and present selected-hotel rates Call `hotel-rates` with the literal hotel `id` from search and the same dates, adult count, and traveler target: ```bash ramp travel hotel-rates "<selected_hotel_id>" --output json \ --check_in_date 2026-08-10 --check_out_date 2026-08-13 \ --rationale "fetch current rooms and rates for the selected Chicago hotel, Aug 10-13" ``` For delegated booking, pass the same `--traveler_user_id`. Keep `--num_adults` consistent with search. Render one row for every returned `all_rates[].rates[]` option: | # | Room | Nightly (pre-tax) | All-in nightly | All-in total | Payment | Refundability | Cancellation | Policy | Loyalty | Notes | |---|------|---------------------|----------------|--------------|---------|---------------|--------------|--------|---------|-------| | 1 | Deluxe King | $245 USD | $271 USD | $812 USD | Pay later | Refundable | Free until Aug 8 | In policy | Four Seasons | Recommended; Company preferred | - Use the room group's `room_name`, `room_description`, and `room_amenities`. Mark a group as recommended only when it also appears in `display_ui_recommended_cards`; do not infer recommendations from list order or `best_rate`. `best_rate` is the lowest-total option within that room group, not a global recommendation. - Show each rate's `nightly_amount`, `all_in_nightly_amount`, `total_amount`, `payment_type`, `refundability`, and `cancellation_policy` when present. Show amount strings exactly as returned and include the separate `currency` once; do not append it when the amount already includes it. - Keep policy concise in the table and surface full `policy_violations` when the traveler compares or selects an out-of-policy rate. - For loyalty, show `loyalty_program` and `earns_loyalty_points` when relevant. Clearly flag `loyalty_required=true` and stop before preview when `loyalty_eligible=false` until the membership is corrected. - Put `is_company_preferred`, `is_corporate_rate`, and useful room amenities in Notes when present. `has_corporate_rates` is context, not proof every rate is corporate. - Keep a private display-number-to-literal-rate-`id` map. Never use a room ID, hotel ID, or search summary rate ID for booking. - Use top-level `hotel` metadata to reconfirm the selected property. `has_corporate_rates` is hotel-level context, while each rate's `is_corporate_rate` identifies the actual corporate rate. These rates are current options, not quotes. Wait for the traveler to select one exact room/rate; never guess or default to the first or recommended row. ## Preview the selected rate Call `book-hotel` without `--confirm`. Its positional values are the literal selected hotel ID from search and selected rate ID from `hotel-rates`. Pass the same stay dates used to fetch the rate: ```bash ramp travel book-hotel "<selected_hotel_id>" "<selected_rate_id>" --output json \ --check_in_date 2026-08-10 --check_out_date 2026-08-13 \ --rationale "preview the selected Chicago hotel rate, Aug 10-13" ``` For delegated booking, pass the same `--traveler_user_id`. If Ramp says the rate expired or is missing from cache, start a fresh `search-hotel`; if the rate mismatches the selected hotel or stay dates, call `hotel-rates` again for that hotel/current dates. Never retry an old rate or silently choose another. The preview is authoritative and may differ from the rates response. Present: - hotel, room, and stay dates - `nightly_rate` and exact all-in `total_amount` - `in_policy` and every `policy_violations` entry - `requires_approval` and each `approval_steps` entry - every `eligible_funds` option: `fund_name` and available balance/spending limit when present; keep a private display-number-to-`fund_uuid` map rather than printing UUIDs - matching `loyalty_programs`: meaningful `display_name` and `loyalty_number` values that Ramp will submit on confirm Present funds without internal IDs: | # | Fund | Available balance | Spending limit | |---|------|-------------------|----------------| | 1 | Client Travel | $1,500 USD | $5,000 USD | Keep the display-number-to-`fund_uuid` map private. For loyalty memberships, show `display_name` and only the last four characters of `loyalty_number` unless the traveler explicitly asks for the full saved number; never display `loyalty_program_id` or `logo` as text. Carry forward refundability, cancellation, and payment details from the selected rate. If that rate requires loyalty but the traveler is not eligible, stop before preview and ask them to correct the membership or choose another rate. Use preview `loyalty_programs` to verify which saved matching membership Ramp will submit, rather than treating the rate's generic program name as proof of enrollment. If the preview is out of policy, show every returned violation and ask why the traveler needs that rate. Keep their exact justification as `oop_reason`; Ramp requires `--oop_reason` on confirmation for an out-of-policy quote and approvers see it. `reason` is a separate optional general booking note; include `--reason` only when the traveler volunteered one. If the traveler explicitly selected a known fund before preview, pass its UUID as `--spend_allocation_id`. Otherwise preview without a fund, present every eligible choice, and ask whether to use one or request a new allocation. If they choose a returned fund, repeat the preview with its literal `fund_uuid` as `--spend_allocation_id`, present any changed approval result, and get explicit approval of that final preview. Confirm with the same fund. Omitting the flag on confirm requests a new allocation. Approval requirements come from `requires_approval` and `approval_steps` and can differ depending on fund selection. If the traveler selected an existing trip, keep its literal UUID in context but do not send `--trip_id` during preview: Ramp only resolves it during confirm. A missing, invalid, or other-user trip is ignored and Ramp auto-selects or creates one. Verify the resulting `trip_id`/`trip_name` after booking when attachment matters. Omitting `--confirm` is the preview flow. Finish with an explicit confirmation question containing the hotel, room, dates, refundability, cancellation terms, all-in total, policy/approval state, matching loyalty membership, selected fund behavior, and OOP justification when applicable. Stop and wait for a clear yes. ## Confirm only after explicit approval Use the same hotel ID, rate ID, dates, traveler, and fund selection from the approved final preview. Add `--confirm` and the preview's exact `total_amount`: ```bash ramp travel book-hotel "<selected_hotel_id>" "<selected_rate_id>" --confirm \ --check_in_date 2026-08-10 --check_out_date 2026-08-13 \ --expected_total_amount '<preview_total_amount>' --output json \ --rationale "book the selected Chicago hotel rate; traveler approved the preview" ``` Add optional confirmation flags only when applicable: - `--traveler_user_id '<traveler_uuid>'`: exact delegated traveler UUID used for fresh search, rates, and preview. - `--spend_allocation_id '<fund_uuid>'`: exact private UUID for the fund selected from the final preview; omit to request a new allocation. - `--trip_id '<trip_uuid>'`: exact existing trip UUID explicitly selected by the traveler; omit to let Ramp auto-select or create a trip. This is the step where Ramp resolves it. - `--oop_reason '<traveler_justification>'`: exact justification collected after an out-of-policy preview; required only when `in_policy=false`. - `--reason '<booking_note>'`: separate optional general note, only when volunteered by the traveler. Never repurpose it as the OOP justification. Keep any collected OOP justification in context through a fund or price re-preview, but send it only on confirmation. If the new preview changes policy violations materially, reconfirm that the traveler's justification still applies. Single-quote currency strings containing `$` so the shell does not expand them. Never normalize, reformat, or recalculate `expected_total_amount`. If confirmation reports a changed total, do not book at the new price. Run a new preview, present the new nightly/all-in amounts and any changed policy terms, then get explicit approval again. For missing/expired cache mappings, start a fresh search; for hotel/date mismatches, fetch fresh rates for the selected hotel/current dates. Treat feature-disabled errors separately from no results. On a successful confirmation, present `booking.booking_request_id`, lowercase `booking.status`, `booking.total_amount`, and `booking.next_steps` before verification. Do not call the reservation confirmed merely because `booked=true`. ## Verify the booking request The confirm response's `booked=true` means a booking request was created, not necessarily that the hotel is confirmed. Show the returned booking status and approval state accurately. Verify with: ```bash ramp travel bookings --output json \ --rationale "verify the Chicago hotel booking request, Aug 10-13" ``` For delegated booking, pass the same traveler UUID. The default call includes current/upcoming hotels, flights, and cars; inspect the hotel results and match using `hotel_name`, dates, `room_type`, and the most recent `booked_at`. Use returned `trip_id` and `trip_name` to verify trip attachment when needed. The submit response's nested `booking.status` is lowercase (`pending_approval`, `approved`, `booked`, or `rejected`). The bookings response uses uppercase request/reservation states: - `CONFIRMED`: report the reservation as confirmed. - `PENDING_APPROVAL`: report that the request is awaiting approval, not booked. - `PROCESSING`: report that fulfillment is still processing and check again later. - `FAILED`: show `error_message` and address the stated issue before retrying. - `CANCELLED`: report that the request/reservation was cancelled. - `REJECTED`: report that the request was rejected. Default bookings lookup excludes unsuccessful `FAILED`, `CANCELLED`, and `REJECTED` requests. Only repeat it with `--include_failed` when diagnosing an explicitly unsuccessful attempt; do not use that flag for normal post-confirm verification. For cancellations, changes, or refunds, hand off to the Ramp web app; do not submit another booking.