Skip to content
Beta. VectorStep is in beta.Let us know if something breaks.

Tutorial: call an API and use its response

executor: webhook posts a rendered prompt_template to a URL — the obvious tool for firing a Slack/PagerDuty notification. It’s less obvious that the same executor is also the right tool for calling a REST API and using its response, not just firing at one: set parse_json: true and the response’s JSON fields become individually addressable from a later step, exactly like an agent’s own JSON reply is.

This tutorial is standalone — no agent, no Gateway, no MCP tools, no LLM call anywhere in it. Just the quick start and two public API calls.

A two-step api-response-demo pipeline: fetch-user calls a public test API and gets a JSON object back; echo-summary references three of its fields in its own request body to a second API, proving the fields actually carried across. Both steps hit jsonplaceholder.typicode.com — a free, public, fake-data API built for exactly this kind of testing (its POST endpoint accepts anything and echoes it back with a fake id; nothing is actually persisted server-side).

pipelines/api-response-demo.yaml:

name: api-response-demo
description: Calls a public JSON API, then uses its response fields in a following step
version: 1
stage: testing
trigger:
match:
source: demo
steps:
- name: fetch-user
executor: webhook
executor_config:
url: https://jsonplaceholder.typicode.com/users/1
method: GET
parse_json: true
confidence_threshold: 0.0
on_low_confidence: proceed
- name: echo-summary
executor: webhook
executor_config:
url: https://jsonplaceholder.typicode.com/posts
method: POST
parse_json: true
confidence_threshold: 0.0
on_low_confidence: proceed
prompt_template: |
{
"title": "Contact lookup for {{steps.fetch_user.name}}",
"body": "Email: {{steps.fetch_user.email}} — Phone: {{steps.fetch_user.phone}} — Website: {{steps.fetch_user.website}}"
}

fetch-user needs no prompt_template at all — it’s a GET, and prompt_template defaults to an empty string. echo-summary’s body is where the real point of this tutorial lives: {{steps.fetch_user.name}}, {{steps.fetch_user.email}}, and {{steps.fetch_user.phone}} reference fields that came from fetch-user’s HTTP response, not from an agent’s JSON reply — parse_json: true is what made them exist as template variables at all. Without it, fetch-user’s output would carry only a 200-character text preview in next_step_context, and none of name, email, or phone would be reachable here.

Terminal window
curl -X POST http://localhost:8000/reload \
-H "Authorization: Bearer $(grep VECTORSTEP_ADMIN_TOKEN ~/.vectorstep/.env | cut -d= -f2)"
Terminal window
curl -X POST "http://localhost:8000/webhook?source=generic&allow_testing=true" \
-H "Authorization: Bearer $(grep VECTORSTEP_WEBHOOK_TOKEN ~/.vectorstep/.env | cut -d= -f2)" \
-H "Content-Type: application/json" \
-d '{"pipeline": "api-response-demo"}'

This was run for real — both HTTP calls actually made, nothing simulated. fetch-user completes in well under a second:

{
"confidence": 1.0,
"summary": "Webhook delivered: HTTP 200",
"next_step_context": "{\n \"id\": 1,\n \"name\": \"Leanne Graham\", ...",
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "[email protected]",
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"address": { "street": "Kulas Light", "city": "Gwenborough", "...": "..." },
"company": { "name": "Romaguera-Crona", "...": "..." }
}

Every top-level field from the API’s JSON response — name, email, phone, website, even the nested address/company objects — sits alongside the usual confidence/summary/next_step_context fields, because that’s exactly what parse_json: true does: splat the response object’s fields onto the step’s output, the same mechanism an agent’s own JSON reply uses.

echo-summary proves those fields actually crossed the step boundary — its own request body, sent to the second API, was rendered with real values from the first:

{
"confidence": 1.0,
"summary": "Webhook delivered: HTTP 201",
"next_step_context": "{\n \"title\": \"Contact lookup for Leanne Graham\", ...",
"title": "Contact lookup for Leanne Graham",
"body": "Email: [email protected] — Phone: 1-770-736-8031 x56442 — Website: hildegard.org",
"id": 101
}

title/body are exactly what echo-summary’s prompt_template rendered to before it was sent — proof the Jinja substitution pulled real values out of fetch-user’s response, not a fixture. id: 101 is jsonplaceholder’s own fake-creation echo, splatted onto echo-summary’s output the same way — if a third step existed, {{steps.echo_summary.id}} would be available to it too.

  • Executors — webhook — the full executor_config schema, and exactly which fields parse_json can never let a response override (confidence, summary, next_step_context, raw_response — dropped with a warning, never merged, if the API’s own JSON happens to use those names).
  • LLMOutput: the step contract — how extra fields propagate to downstream templates, regardless of which executor produced them.
  • Tutorial: run deterministic logic as a pipeline step — the sibling non-LLM step type. If what you need is local, deterministic logic (a threshold check, a transform) rather than an HTTP call to something else, that’s the one to reach for instead.
  • Choosing an executor for external calls — the fuller comparison of gateway/webhook/shell and why trust model, not raw capability, is what actually decides between them.