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.
What you’ll build
Section titled “What you’ll build”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).
1. Write the pipeline
Section titled “1. Write the pipeline”pipelines/api-response-demo.yaml:
name: api-response-demodescription: Calls a public JSON API, then uses its response fields in a following stepversion: 1stage: 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.
2. Reload
Section titled “2. Reload”curl -X POST http://localhost:8000/reload \ -H "Authorization: Bearer $(grep VECTORSTEP_ADMIN_TOKEN ~/.vectorstep/.env | cut -d= -f2)"3. Trigger it
Section titled “3. Trigger it”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"}'What you should see
Section titled “What you should see”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", "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", "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.
Where next
Section titled “Where next”- Executors —
webhook— the fullexecutor_configschema, and exactly which fieldsparse_jsoncan 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/shelland why trust model, not raw capability, is what actually decides between them.