How to Do Remote API Calls with Potentially Expiring Tokens#
Prerequisites This guide assumes familiarity with:
When buidling assistants with tools that reply on remote API calls, it is important to handle the authentication failures gracefully—especially those caused by expired access tokens. In this guide, you will build an assistant that calls a mock service requiring a valid token for authentication.
Setup#
To demonstrate the concept in a safe environment, we first set up a local mock API server (here, using Starlette). This simulates an endpoint that requires and validates an authentication token. If the token provided is:
a valid token (valid-token): the service responds with a success message.
an expired token (expired-token) or invalid token: the 401 Unauthorized error is returned with details.
1from starlette.applications import Starlette
2from starlette.responses import JSONResponse
3from starlette.requests import Request
4from starlette.routing import Route
5from starlette.exceptions import HTTPException
6from starlette.status import HTTP_401_UNAUTHORIZED
7
8async def protected_endpoint(request: Request):
9 user = request.query_params.get("user")
10 if user is None:
11 return JSONResponse({"detail": "Missing 'user' query parameter."}, status_code=400)
12
13 authorization = request.headers.get("authorization")
14 if authorization is None or not authorization.startswith("Bearer "):
15 raise HTTPException(
16 status_code=HTTP_401_UNAUTHORIZED,
17 detail="Missing or malformed Authorization header.",
18 headers={"WWW-Authenticate": "Bearer"},
19 )
20
21 token = authorization.split(" ")[1]
22 if token == "valid-token":
23 return JSONResponse({"response": f"Success! You are authenticated, {user}."})
24 elif token == "expired-token":
25 raise HTTPException(
26 status_code=HTTP_401_UNAUTHORIZED,
27 detail="Token has expired.",
28 headers={"WWW-Authenticate": "Bearer error='invalid_token', error_description='The access token expired'"},
29 )
30 else:
31 raise HTTPException(
32 status_code=HTTP_401_UNAUTHORIZED,
33 detail="Invalid access token.",
34 headers={"WWW-Authenticate": "Bearer error='invalid_token'"},
35 )
36
37app = Starlette(debug=True, routes=[
38 Route("/protected", protected_endpoint)
39])
40
41# Start the server: Uncomment these lines
42# import uvicorn
43# uvicorn.run(app, host="localhost", port=8001)
Basic implementation#
In this example, you will build a simple Agent that includes a Flow with three steps:
A start step to get the user name
A step to trigger a client tool that collects a token from the user
A step to call a remote API given the user name and the token
This guide requires the use of an LLM. WayFlow supports several LLM API providers. Select an LLM from the options below:
from wayflowcore.models import OCIGenAIModel
if __name__ == "__main__":
llm = OCIGenAIModel(
model_id="provider.model-id",
service_endpoint="https://url-to-service-endpoint.com",
compartment_id="compartment-id",
auth_type="API_KEY",
)
from wayflowcore.models import VllmModel
llm = VllmModel(
model_id="model-id",
host_port="VLLM_HOST_PORT",
)
from wayflowcore.models import OllamaModel
llm = OllamaModel(
model_id="model-id",
)
Importing libraries#
First import what is needed for this tutorial:
1from wayflowcore.property import StringProperty
2from wayflowcore.tools import ClientTool
3from wayflowcore.steps import (
4 StartStep,
5 CompleteStep,
6 ApiCallStep,
7 ToolExecutionStep
8)
9from wayflowcore.flow import Flow
10from wayflowcore.controlconnection import ControlFlowEdge
11from wayflowcore.dataconnection import DataFlowEdge
Creating the steps#
Define the variable names and steps.
1TOKEN = "token"
2USER = "user"
1# 1. Start step
2start_step = StartStep(
3 name="start_step",
4 input_descriptors=[StringProperty(name=USER)]
5)
6
7# 2. Get token step
8# A client tool to get token at client side
9get_token_tool = ClientTool(
10 name="get_token_tool",
11 description="Get token from user",
12 input_descriptors=[],
13 output_descriptors=[StringProperty(name=TOKEN)]
14)
15
16# A step gets token by using the get_token_tool
17get_token_tool_step = ToolExecutionStep(
18 name="get_token_step",
19 tool=get_token_tool,
20)
21
22# 3. Call API step
23call_api_step = ApiCallStep(
24 name="call_api_step",
25 url="http://localhost:8003/protected",
26 allow_insecure_http=True,
27 method="GET",
28 headers={"Authorization": "Bearer {{ token }}"},
29 params={"user": "{{ user }}"},
30)
31
32# 4. End step
33end_step = CompleteStep(name="end_step")
In this simple example, we manually input the user name and the token in the code. For a more interactive approach, consider using InputMessageStep to prompt the user to enter these values during execution.
Creating the flow#
1remote_call_flow = Flow(
2 name="Remote Call Flow",
3 description="Perform a call to a remote endpoint given the `user` parameter.",
4 begin_step=start_step,
5 control_flow_edges=[
6 ControlFlowEdge(source_step=start_step, destination_step=get_token_tool_step),
7 ControlFlowEdge(source_step=get_token_tool_step, destination_step=call_api_step),
8 ControlFlowEdge(source_step=call_api_step, destination_step=end_step),
9 ],
10 data_flow_edges=[
11 DataFlowEdge(
12 source_step=start_step,
13 source_output=USER,
14 destination_step=call_api_step,
15 destination_input=USER,
16 ),
17 DataFlowEdge(
18 source_step=get_token_tool_step,
19 source_output=TOKEN,
20 destination_step=call_api_step,
21 destination_input=TOKEN,
22 ),
23 ]
24)
This flow simply proceeds through three steps as defined in the control_flow_edges
.
The data_flow_edges
connect the outputs of each step—the user name from start_step
and the token from get_token_tool_step
—to the inputs required by call_api_step
.
Testing the flow#
1from wayflowcore.executors.executionstatus import ToolRequestStatus
2from wayflowcore.tools import ToolResult
3
4inputs = {"user": "alice"}
5conversation = remote_call_flow.start_conversation(inputs=inputs)
6
7auth_token = "valid-token"
8# auth_token = "expired-token" # This will raise error
9
10status = conversation.execute()
11if isinstance(status, ToolRequestStatus): # Asking for token
12 tool_request_id = status.tool_requests[0].tool_request_id # Need to be adapted when using parallel tool calling (not the case here)
13 conversation.append_tool_result(ToolResult(content=auth_token, tool_request_id=tool_request_id))
14else:
15 print(
16 f"Invalid execution status, expected ToolRequestStatus, received {type(status)}"
17 )
To simulate a valid user, provide auth_token = "valid-token"
.
To test expiry handling, use auth_token = "expired-token"
, which is expected to raise an error.
The flow should pause at the token step, mimicking a credential input prompt, then proceed upon receiving input.
Creating an agent#
Now, create an agent that includes the defined flow:
1from wayflowcore.agent import Agent
2
3agent = Agent(
4 name="Agent",
5 flows=[remote_call_flow],
6 llm=llm,
7)
Testing the agent#
1from wayflowcore.executors.executionstatus import ToolRequestStatus, UserMessageRequestStatus
2from wayflowcore.tools import ToolResult
3
4conversation = agent.start_conversation()
5conversation.append_user_message("Call the remote tool with user `alice`")
6
7auth_token = "valid-token"
8# auth_token = "expired-token" # This will raise error
9status = conversation.execute()
10
11if isinstance(status, ToolRequestStatus): # Asking for token
12 tool_request_id = status.tool_requests[0].tool_request_id # Needs to be adapted when using parallel tool calling (not the case here)
13 conversation.append_tool_result(ToolResult(content=auth_token, tool_request_id=tool_request_id))
14else:
15 print(
16 f"Invalid execution status, expected ToolRequestStatus, received {type(status)}"
17 )
18
19status = conversation.execute() # Resuming the conversation after the client provided the auth token
20if isinstance(status, UserMessageRequestStatus):
21 assistant_reply = conversation.get_last_message()
22 print(f"---\nAssistant >>> {assistant_reply.content}\n---")
23else:
24 print(
25 f"Invalid execution status, expected UserMessageRequestStatus, received {type(status)}"
26 )
The code block above demonstrates an interaction flow between a user and the agent, simulating how the assistant processes a remote, authenticated API call.
During the first execution, the agent determines that a token is required and issues a tool request to the client. This is reflected by the status
being an instance of ToolRequestStatus
.
After the client provides the required credential (the token), the second execution resumes the conversation.
If authentication is successful, the agent proceeds to call the API, processes the response, and generates a user message as its reply.
At this stage, the status
should be UserMessageRequestStatus
, which indicates that the agent has completed processing and is now ready to present a message to the user or wait for the next user prompt.
Checking for UserMessageRequestStatus
ensures that your code only tries to access the assistant’s reply when it is actually available.
Agent Spec Exporting/Loading#
You can export the assistant configuration to its Agent Spec configuration using the AgentSpecExporter
.
from wayflowcore.agentspec import AgentSpecExporter
serialized_assistant = AgentSpecExporter().to_json(agent)
Here is what the Agent Spec representation will look like ↓
Click here to see the assistant configuration.
{
"component_type": "ExtendedAgent",
"id": "f3afdfe1-4b5b-469e-9505-33f72b3cd342",
"name": "Agent",
"description": "",
"metadata": {
"__metadata_info__": {
"name": "Agent",
"description": ""
}
},
"inputs": [],
"outputs": [],
"llm_config": {
"component_type": "VllmConfig",
"id": "ab50c1e4-d6f3-493b-84b6-c570da3c7464",
"name": "LLAMA_MODEL_ID",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"default_generation_parameters": null,
"url": "LLAMA_API_URL",
"model_id": "LLAMA_MODEL_ID"
},
"system_prompt": "",
"tools": [],
"toolboxes": [],
"context_providers": null,
"can_finish_conversation": false,
"max_iterations": 10,
"initial_message": "Hi! How can I help you?",
"caller_input_mode": "always",
"agents": [],
"flows": [
{
"component_type": "Flow",
"id": "8e8bc09a-6b3c-47c3-b8b8-ec64743beca8",
"name": "Remote Call Flow",
"description": "Perform a call to a remote endpoint given the `user` parameter.",
"metadata": {
"__metadata_info__": {}
},
"inputs": [
{
"type": "string",
"title": "user"
}
],
"outputs": [
{
"type": "string",
"title": "token"
},
{
"description": "returned http status code",
"type": "integer",
"title": "http_status_code"
}
],
"start_node": {
"$component_ref": "2b14ec5f-efe2-4b00-bfd9-1a7070b501b7"
},
"nodes": [
{
"$component_ref": "2b14ec5f-efe2-4b00-bfd9-1a7070b501b7"
},
{
"$component_ref": "d4b896d7-fa3d-49ec-a45c-79021bf74e28"
},
{
"$component_ref": "ed43def1-9d9b-4f15-84a3-cc9068143021"
},
{
"$component_ref": "969d49b1-785b-4108-bd1e-6082b09160da"
}
],
"control_flow_connections": [
{
"component_type": "ControlFlowEdge",
"id": "f44393b4-b9fb-4acd-9dc9-f4d496ccecae",
"name": "start_step_to_get_token_step_control_flow_edge",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"from_node": {
"$component_ref": "2b14ec5f-efe2-4b00-bfd9-1a7070b501b7"
},
"from_branch": null,
"to_node": {
"$component_ref": "d4b896d7-fa3d-49ec-a45c-79021bf74e28"
}
},
{
"component_type": "ControlFlowEdge",
"id": "212e435b-059c-4842-9204-ed3b02f2bd17",
"name": "get_token_step_to_call_api_step_control_flow_edge",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"from_node": {
"$component_ref": "d4b896d7-fa3d-49ec-a45c-79021bf74e28"
},
"from_branch": null,
"to_node": {
"$component_ref": "ed43def1-9d9b-4f15-84a3-cc9068143021"
}
},
{
"component_type": "ControlFlowEdge",
"id": "1b509ffa-24fe-4cda-a441-5456675ad64a",
"name": "call_api_step_to_end_step_control_flow_edge",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"from_node": {
"$component_ref": "ed43def1-9d9b-4f15-84a3-cc9068143021"
},
"from_branch": null,
"to_node": {
"$component_ref": "969d49b1-785b-4108-bd1e-6082b09160da"
}
}
],
"data_flow_connections": [
{
"component_type": "DataFlowEdge",
"id": "a2efbfb4-7046-4cba-b88f-010adaa77784",
"name": "start_step_user_to_call_api_step_user_data_flow_edge",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"source_node": {
"$component_ref": "2b14ec5f-efe2-4b00-bfd9-1a7070b501b7"
},
"source_output": "user",
"destination_node": {
"$component_ref": "ed43def1-9d9b-4f15-84a3-cc9068143021"
},
"destination_input": "user"
},
{
"component_type": "DataFlowEdge",
"id": "6d1baa26-611e-48d3-8021-c2ef39ec60d4",
"name": "get_token_step_token_to_call_api_step_token_data_flow_edge",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"source_node": {
"$component_ref": "d4b896d7-fa3d-49ec-a45c-79021bf74e28"
},
"source_output": "token",
"destination_node": {
"$component_ref": "ed43def1-9d9b-4f15-84a3-cc9068143021"
},
"destination_input": "token"
},
{
"component_type": "DataFlowEdge",
"id": "91b4c20c-93e6-4238-8dcd-28aea890788c",
"name": "get_token_step_token_to_end_step_token_data_flow_edge",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"source_node": {
"$component_ref": "d4b896d7-fa3d-49ec-a45c-79021bf74e28"
},
"source_output": "token",
"destination_node": {
"$component_ref": "969d49b1-785b-4108-bd1e-6082b09160da"
},
"destination_input": "token"
},
{
"component_type": "DataFlowEdge",
"id": "1c10dfaa-f6f9-4634-9b83-7e286996650c",
"name": "call_api_step_http_status_code_to_end_step_http_status_code_data_flow_edge",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"source_node": {
"$component_ref": "ed43def1-9d9b-4f15-84a3-cc9068143021"
},
"source_output": "http_status_code",
"destination_node": {
"$component_ref": "969d49b1-785b-4108-bd1e-6082b09160da"
},
"destination_input": "http_status_code"
}
],
"$referenced_components": {
"ed43def1-9d9b-4f15-84a3-cc9068143021": {
"component_type": "ApiNode",
"id": "ed43def1-9d9b-4f15-84a3-cc9068143021",
"name": "call_api_step",
"description": "",
"metadata": {
"__metadata_info__": {}
},
"inputs": [
{
"description": "string template variable named user",
"type": "string",
"title": "user"
},
{
"description": "string template variable named token",
"type": "string",
"title": "token"
}
],
"outputs": [
{
"description": "returned http status code",
"type": "integer",
"title": "http_status_code"
}
],
"branches": [
"next"
],
"url": "http://localhost:8003/protected",
"http_method": "GET",
"api_spec_uri": null,
"data": {},
"query_params": {
"user": "{{ user }}"
},
"headers": {
"Authorization": "Bearer {{ token }}"
}
},
"2b14ec5f-efe2-4b00-bfd9-1a7070b501b7": {
"component_type": "StartNode",
"id": "2b14ec5f-efe2-4b00-bfd9-1a7070b501b7",
"name": "start_step",
"description": "",
"metadata": {
"__metadata_info__": {}
},
"inputs": [
{
"type": "string",
"title": "user"
}
],
"outputs": [
{
"type": "string",
"title": "user"
}
],
"branches": [
"next"
]
},
"d4b896d7-fa3d-49ec-a45c-79021bf74e28": {
"component_type": "ExtendedToolNode",
"id": "d4b896d7-fa3d-49ec-a45c-79021bf74e28",
"name": "get_token_step",
"description": "",
"metadata": {
"__metadata_info__": {}
},
"inputs": [],
"outputs": [
{
"type": "string",
"title": "token"
}
],
"branches": [
"next"
],
"tool": {
"component_type": "ClientTool",
"id": "41399e99-d42a-45c1-adc5-d3d016bd5a1c",
"name": "get_token_tool",
"description": "Get token from user",
"metadata": {
"__metadata_info__": {}
},
"inputs": [],
"outputs": [
{
"type": "string",
"title": "token"
}
]
},
"input_mapping": {},
"output_mapping": {},
"raise_exceptions": false,
"component_plugin_name": "NodesPlugin",
"component_plugin_version": "25.4.0.dev0"
},
"969d49b1-785b-4108-bd1e-6082b09160da": {
"component_type": "EndNode",
"id": "969d49b1-785b-4108-bd1e-6082b09160da",
"name": "end_step",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"inputs": [
{
"type": "string",
"title": "token"
},
{
"description": "returned http status code",
"type": "integer",
"title": "http_status_code"
}
],
"outputs": [
{
"type": "string",
"title": "token"
},
{
"description": "returned http status code",
"type": "integer",
"title": "http_status_code"
}
],
"branches": [],
"branch_name": "end_step"
}
}
}
],
"agent_template": {
"component_type": "PluginPromptTemplate",
"id": "95b539ce-0908-4a45-9ab0-cd6fa1b346d7",
"name": "",
"description": "",
"metadata": {
"__metadata_info__": {}
},
"messages": [
{
"role": "system",
"contents": [
{
"type": "text",
"content": "{% if custom_instruction %}{{custom_instruction}}{% endif %}"
}
],
"tool_requests": null,
"tool_result": null,
"display_only": false,
"sender": null,
"recipients": [],
"time_created": "2025-09-02T15:52:22.014400+00:00",
"time_updated": "2025-09-02T15:52:22.014401+00:00"
},
{
"role": "user",
"contents": [],
"tool_requests": null,
"tool_result": null,
"display_only": false,
"sender": null,
"recipients": [],
"time_created": "2025-09-02T15:52:22.008803+00:00",
"time_updated": "2025-09-02T15:52:22.010218+00:00"
},
{
"role": "system",
"contents": [
{
"type": "text",
"content": "{% if __PLAN__ %}The current plan you should follow is the following: \n{{__PLAN__}}{% endif %}"
}
],
"tool_requests": null,
"tool_result": null,
"display_only": false,
"sender": null,
"recipients": [],
"time_created": "2025-09-02T15:52:22.014421+00:00",
"time_updated": "2025-09-02T15:52:22.014421+00:00"
}
],
"output_parser": null,
"inputs": [
{
"description": "\"custom_instruction\" input variable for the template",
"type": "string",
"title": "custom_instruction",
"default": ""
},
{
"description": "\"__PLAN__\" input variable for the template",
"type": "string",
"title": "__PLAN__",
"default": ""
},
{
"type": "array",
"items": {},
"title": "__CHAT_HISTORY__"
}
],
"pre_rendering_transforms": null,
"post_rendering_transforms": [
{
"component_type": "PluginRemoveEmptyNonUserMessageTransform",
"id": "372d6f16-b945-4b10-b1c8-adc143ddab9d",
"name": "removeemptynonusermessage_messagetransform",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"component_plugin_name": "MessageTransformPlugin",
"component_plugin_version": "25.4.0.dev0"
}
],
"tools": null,
"native_tool_calling": true,
"response_format": null,
"native_structured_generation": true,
"generation_config": null,
"component_plugin_name": "PromptTemplatePlugin",
"component_plugin_version": "25.4.0.dev0"
},
"component_plugin_name": "AgentPlugin",
"component_plugin_version": "25.4.0.dev0",
"agentspec_version": "25.4.1"
}
component_type: ExtendedAgent
id: f3afdfe1-4b5b-469e-9505-33f72b3cd342
name: Agent
description: ''
metadata:
__metadata_info__:
name: Agent
description: ''
inputs: []
outputs: []
llm_config:
component_type: VllmConfig
id: ab50c1e4-d6f3-493b-84b6-c570da3c7464
name: LLAMA_MODEL_ID
description: null
metadata:
__metadata_info__: {}
default_generation_parameters: null
url: LLAMA_API_URL
model_id: LLAMA_MODEL_ID
system_prompt: ''
tools: []
toolboxes: []
context_providers: null
can_finish_conversation: false
max_iterations: 10
initial_message: Hi! How can I help you?
caller_input_mode: always
agents: []
flows:
- component_type: Flow
id: 8e8bc09a-6b3c-47c3-b8b8-ec64743beca8
name: Remote Call Flow
description: Perform a call to a remote endpoint given the `user` parameter.
metadata:
__metadata_info__: {}
inputs:
- type: string
title: user
outputs:
- type: string
title: token
- description: returned http status code
type: integer
title: http_status_code
start_node:
$component_ref: 2b14ec5f-efe2-4b00-bfd9-1a7070b501b7
nodes:
- $component_ref: 2b14ec5f-efe2-4b00-bfd9-1a7070b501b7
- $component_ref: d4b896d7-fa3d-49ec-a45c-79021bf74e28
- $component_ref: ed43def1-9d9b-4f15-84a3-cc9068143021
- $component_ref: 969d49b1-785b-4108-bd1e-6082b09160da
control_flow_connections:
- component_type: ControlFlowEdge
id: f44393b4-b9fb-4acd-9dc9-f4d496ccecae
name: start_step_to_get_token_step_control_flow_edge
description: null
metadata:
__metadata_info__: {}
from_node:
$component_ref: 2b14ec5f-efe2-4b00-bfd9-1a7070b501b7
from_branch: null
to_node:
$component_ref: d4b896d7-fa3d-49ec-a45c-79021bf74e28
- component_type: ControlFlowEdge
id: 212e435b-059c-4842-9204-ed3b02f2bd17
name: get_token_step_to_call_api_step_control_flow_edge
description: null
metadata:
__metadata_info__: {}
from_node:
$component_ref: d4b896d7-fa3d-49ec-a45c-79021bf74e28
from_branch: null
to_node:
$component_ref: ed43def1-9d9b-4f15-84a3-cc9068143021
- component_type: ControlFlowEdge
id: 1b509ffa-24fe-4cda-a441-5456675ad64a
name: call_api_step_to_end_step_control_flow_edge
description: null
metadata:
__metadata_info__: {}
from_node:
$component_ref: ed43def1-9d9b-4f15-84a3-cc9068143021
from_branch: null
to_node:
$component_ref: 969d49b1-785b-4108-bd1e-6082b09160da
data_flow_connections:
- component_type: DataFlowEdge
id: a2efbfb4-7046-4cba-b88f-010adaa77784
name: start_step_user_to_call_api_step_user_data_flow_edge
description: null
metadata:
__metadata_info__: {}
source_node:
$component_ref: 2b14ec5f-efe2-4b00-bfd9-1a7070b501b7
source_output: user
destination_node:
$component_ref: ed43def1-9d9b-4f15-84a3-cc9068143021
destination_input: user
- component_type: DataFlowEdge
id: 6d1baa26-611e-48d3-8021-c2ef39ec60d4
name: get_token_step_token_to_call_api_step_token_data_flow_edge
description: null
metadata:
__metadata_info__: {}
source_node:
$component_ref: d4b896d7-fa3d-49ec-a45c-79021bf74e28
source_output: token
destination_node:
$component_ref: ed43def1-9d9b-4f15-84a3-cc9068143021
destination_input: token
- component_type: DataFlowEdge
id: 91b4c20c-93e6-4238-8dcd-28aea890788c
name: get_token_step_token_to_end_step_token_data_flow_edge
description: null
metadata:
__metadata_info__: {}
source_node:
$component_ref: d4b896d7-fa3d-49ec-a45c-79021bf74e28
source_output: token
destination_node:
$component_ref: 969d49b1-785b-4108-bd1e-6082b09160da
destination_input: token
- component_type: DataFlowEdge
id: 1c10dfaa-f6f9-4634-9b83-7e286996650c
name: call_api_step_http_status_code_to_end_step_http_status_code_data_flow_edge
description: null
metadata:
__metadata_info__: {}
source_node:
$component_ref: ed43def1-9d9b-4f15-84a3-cc9068143021
source_output: http_status_code
destination_node:
$component_ref: 969d49b1-785b-4108-bd1e-6082b09160da
destination_input: http_status_code
$referenced_components:
ed43def1-9d9b-4f15-84a3-cc9068143021:
component_type: ApiNode
id: ed43def1-9d9b-4f15-84a3-cc9068143021
name: call_api_step
description: ''
metadata:
__metadata_info__: {}
inputs:
- description: string template variable named user
type: string
title: user
- description: string template variable named token
type: string
title: token
outputs:
- description: returned http status code
type: integer
title: http_status_code
branches:
- next
url: http://localhost:8003/protected
http_method: GET
api_spec_uri: null
data: {}
query_params:
user: '{{ user }}'
headers:
Authorization: Bearer {{ token }}
2b14ec5f-efe2-4b00-bfd9-1a7070b501b7:
component_type: StartNode
id: 2b14ec5f-efe2-4b00-bfd9-1a7070b501b7
name: start_step
description: ''
metadata:
__metadata_info__: {}
inputs:
- type: string
title: user
outputs:
- type: string
title: user
branches:
- next
d4b896d7-fa3d-49ec-a45c-79021bf74e28:
component_type: ExtendedToolNode
id: d4b896d7-fa3d-49ec-a45c-79021bf74e28
name: get_token_step
description: ''
metadata:
__metadata_info__: {}
inputs: []
outputs:
- type: string
title: token
branches:
- next
tool:
component_type: ClientTool
id: 41399e99-d42a-45c1-adc5-d3d016bd5a1c
name: get_token_tool
description: Get token from user
metadata:
__metadata_info__: {}
inputs: []
outputs:
- type: string
title: token
input_mapping: {}
output_mapping: {}
raise_exceptions: false
component_plugin_name: NodesPlugin
component_plugin_version: 25.4.0.dev0
969d49b1-785b-4108-bd1e-6082b09160da:
component_type: EndNode
id: 969d49b1-785b-4108-bd1e-6082b09160da
name: end_step
description: null
metadata:
__metadata_info__: {}
inputs:
- type: string
title: token
- description: returned http status code
type: integer
title: http_status_code
outputs:
- type: string
title: token
- description: returned http status code
type: integer
title: http_status_code
branches: []
branch_name: end_step
agent_template:
component_type: PluginPromptTemplate
id: 95b539ce-0908-4a45-9ab0-cd6fa1b346d7
name: ''
description: ''
metadata:
__metadata_info__: {}
messages:
- role: system
contents:
- type: text
content: '{% if custom_instruction %}{{custom_instruction}}{% endif %}'
tool_requests: null
tool_result: null
display_only: false
sender: null
recipients: []
time_created: '2025-09-02T15:52:22.014400+00:00'
time_updated: '2025-09-02T15:52:22.014401+00:00'
- role: user
contents: []
tool_requests: null
tool_result: null
display_only: false
sender: null
recipients: []
time_created: '2025-09-02T15:52:22.008803+00:00'
time_updated: '2025-09-02T15:52:22.010218+00:00'
- role: system
contents:
- type: text
content: "{% if __PLAN__ %}The current plan you should follow is the following:\
\ \n{{__PLAN__}}{% endif %}"
tool_requests: null
tool_result: null
display_only: false
sender: null
recipients: []
time_created: '2025-09-02T15:52:22.014421+00:00'
time_updated: '2025-09-02T15:52:22.014421+00:00'
output_parser: null
inputs:
- description: '"custom_instruction" input variable for the template'
type: string
title: custom_instruction
default: ''
- description: '"__PLAN__" input variable for the template'
type: string
title: __PLAN__
default: ''
- type: array
items: {}
title: __CHAT_HISTORY__
pre_rendering_transforms: null
post_rendering_transforms:
- component_type: PluginRemoveEmptyNonUserMessageTransform
id: 372d6f16-b945-4b10-b1c8-adc143ddab9d
name: removeemptynonusermessage_messagetransform
description: null
metadata:
__metadata_info__: {}
component_plugin_name: MessageTransformPlugin
component_plugin_version: 25.4.0.dev0
tools: null
native_tool_calling: true
response_format: null
native_structured_generation: true
generation_config: null
component_plugin_name: PromptTemplatePlugin
component_plugin_version: 25.4.0.dev0
component_plugin_name: AgentPlugin
component_plugin_version: 25.4.0.dev0
agentspec_version: 25.4.1
You can then load the configuration back to an assistant using the AgentSpecLoader
.
from wayflowcore.agentspec import AgentSpecLoader
assistant: Agent = AgentSpecLoader().load_json(serialized_assistant)
Note
This guide uses the following extension/plugin Agent Spec components:
PluginPromptTemplate
PluginRemoveEmptyNonUserMessageTransform
ExtendedToolNode
ExtendedAgent
See the list of available Agent Spec extension/plugin components in the API Reference
Next steps#
In this guide, you learned how to define a simple flow that retrieves a token from the user and uses it to authenticate remote API calls. To continue learning, checkout:
Full code#
1# Copyright © 2025 Oracle and/or its affiliates.
2#
3# This software is under the Universal Permissive License
4# %%[markdown]
5# Code Example - How to Do Remote API Calls with Potentially Expiring Tokens
6# --------------------------------------------------------------------------
7
8# How to use:
9# Create a new Python virtual environment and install the latest WayFlow version.
10# ```bash
11# python -m venv venv-wayflowcore
12# source venv-wayflowcore/bin/activate
13# pip install --upgrade pip
14# pip install "wayflowcore==26.1"
15# ```
16
17# You can now run the script
18# 1. As a Python file:
19# ```bash
20# python howto_remote_tool_expired_token.py
21# ```
22# 2. As a Notebook (in VSCode):
23# When viewing the file,
24# - press the keys Ctrl + Enter to run the selected cell
25# - or Shift + Enter to run the selected cell and move to the cell below# (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl) or Apache License
26# 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0), at your option.
27
28
29
30
31# %%[markdown]
32## Mock server
33
34# %%
35from starlette.applications import Starlette
36from starlette.responses import JSONResponse
37from starlette.requests import Request
38from starlette.routing import Route
39from starlette.exceptions import HTTPException
40from starlette.status import HTTP_401_UNAUTHORIZED
41
42async def protected_endpoint(request: Request):
43 user = request.query_params.get("user")
44 if user is None:
45 return JSONResponse({"detail": "Missing 'user' query parameter."}, status_code=400)
46
47 authorization = request.headers.get("authorization")
48 if authorization is None or not authorization.startswith("Bearer "):
49 raise HTTPException(
50 status_code=HTTP_401_UNAUTHORIZED,
51 detail="Missing or malformed Authorization header.",
52 headers={"WWW-Authenticate": "Bearer"},
53 )
54
55 token = authorization.split(" ")[1]
56 if token == "valid-token":
57 return JSONResponse({"response": f"Success! You are authenticated, {user}."})
58 elif token == "expired-token":
59 raise HTTPException(
60 status_code=HTTP_401_UNAUTHORIZED,
61 detail="Token has expired.",
62 headers={"WWW-Authenticate": "Bearer error='invalid_token', error_description='The access token expired'"},
63 )
64 else:
65 raise HTTPException(
66 status_code=HTTP_401_UNAUTHORIZED,
67 detail="Invalid access token.",
68 headers={"WWW-Authenticate": "Bearer error='invalid_token'"},
69 )
70
71app = Starlette(debug=True, routes=[
72 Route("/protected", protected_endpoint)
73])
74
75# Start the server: Uncomment these lines
76# import uvicorn
77# uvicorn.run(app, host="localhost", port=8001)
78
79# %%[markdown]
80## Import libraries
81
82# %%
83from wayflowcore.property import StringProperty
84from wayflowcore.tools import ClientTool
85from wayflowcore.steps import (
86 StartStep,
87 CompleteStep,
88 ApiCallStep,
89 ToolExecutionStep
90)
91from wayflowcore.flow import Flow
92from wayflowcore.controlconnection import ControlFlowEdge
93from wayflowcore.dataconnection import DataFlowEdge
94
95# %%[markdown]
96## Configure your LLM
97
98# %%
99from wayflowcore.models import VllmModel
100llm = VllmModel(
101 model_id="LLAMA_MODEL_ID",
102 host_port="LLAMA_API_URL",
103)
104
105# %%[markdown]
106## Variable names
107
108# %%
109TOKEN = "token"
110USER = "user"
111
112# %%[markdown]
113## Defining steps
114
115# %%
116# 1. Start step
117start_step = StartStep(
118 name="start_step",
119 input_descriptors=[StringProperty(name=USER)]
120)
121
122# 2. Get token step
123# A client tool to get token at client side
124get_token_tool = ClientTool(
125 name="get_token_tool",
126 description="Get token from user",
127 input_descriptors=[],
128 output_descriptors=[StringProperty(name=TOKEN)]
129)
130
131# A step gets token by using the get_token_tool
132get_token_tool_step = ToolExecutionStep(
133 name="get_token_step",
134 tool=get_token_tool,
135)
136
137# 3. Call API step
138call_api_step = ApiCallStep(
139 name="call_api_step",
140 url="http://localhost:8003/protected",
141 allow_insecure_http=True,
142 method="GET",
143 headers={"Authorization": "Bearer {{ token }}"},
144 params={"user": "{{ user }}"},
145)
146
147# 4. End step
148end_step = CompleteStep(name="end_step")
149
150# %%[markdown]
151## Defining flow
152
153# %%
154remote_call_flow = Flow(
155 name="Remote Call Flow",
156 description="Perform a call to a remote endpoint given the `user` parameter.",
157 begin_step=start_step,
158 control_flow_edges=[
159 ControlFlowEdge(source_step=start_step, destination_step=get_token_tool_step),
160 ControlFlowEdge(source_step=get_token_tool_step, destination_step=call_api_step),
161 ControlFlowEdge(source_step=call_api_step, destination_step=end_step),
162 ],
163 data_flow_edges=[
164 DataFlowEdge(
165 source_step=start_step,
166 source_output=USER,
167 destination_step=call_api_step,
168 destination_input=USER,
169 ),
170 DataFlowEdge(
171 source_step=get_token_tool_step,
172 source_output=TOKEN,
173 destination_step=call_api_step,
174 destination_input=TOKEN,
175 ),
176 ]
177)
178
179# %%[markdown]
180## Testing flow
181
182# %%
183from wayflowcore.executors.executionstatus import ToolRequestStatus
184from wayflowcore.tools import ToolResult
185
186inputs = {"user": "alice"}
187conversation = remote_call_flow.start_conversation(inputs=inputs)
188
189auth_token = "valid-token"
190# auth_token = "expired-token" # This will raise error
191
192status = conversation.execute()
193if isinstance(status, ToolRequestStatus): # Asking for token
194 tool_request_id = status.tool_requests[0].tool_request_id # Need to be adapted when using parallel tool calling (not the case here)
195 conversation.append_tool_result(ToolResult(content=auth_token, tool_request_id=tool_request_id))
196else:
197 print(
198 f"Invalid execution status, expected ToolRequestStatus, received {type(status)}"
199 )
200
201# %%[markdown]
202## Defining agent
203
204# %%
205from wayflowcore.agent import Agent
206
207agent = Agent(
208 name="Agent",
209 flows=[remote_call_flow],
210 llm=llm,
211)
212
213# %%[markdown]
214## Testing agent
215
216# %%
217from wayflowcore.executors.executionstatus import ToolRequestStatus, UserMessageRequestStatus
218from wayflowcore.tools import ToolResult
219
220conversation = agent.start_conversation()
221conversation.append_user_message("Call the remote tool with user `alice`")
222
223auth_token = "valid-token"
224# auth_token = "expired-token" # This will raise error
225status = conversation.execute()
226
227if isinstance(status, ToolRequestStatus): # Asking for token
228 tool_request_id = status.tool_requests[0].tool_request_id # Needs to be adapted when using parallel tool calling (not the case here)
229 conversation.append_tool_result(ToolResult(content=auth_token, tool_request_id=tool_request_id))
230else:
231 print(
232 f"Invalid execution status, expected ToolRequestStatus, received {type(status)}"
233 )
234
235status = conversation.execute() # Resuming the conversation after the client provided the auth token
236if isinstance(status, UserMessageRequestStatus):
237 assistant_reply = conversation.get_last_message()
238 print(f"---\nAssistant >>> {assistant_reply.content}\n---")
239else:
240 print(
241 f"Invalid execution status, expected UserMessageRequestStatus, received {type(status)}"
242 )
243
244# %%[markdown]
245## Export config to Agent Spec
246
247# %%
248from wayflowcore.agentspec import AgentSpecExporter
249
250serialized_assistant = AgentSpecExporter().to_json(agent)
251
252# %%[markdown]
253## Load Agent Spec config
254
255# %%
256from wayflowcore.agentspec import AgentSpecLoader
257
258assistant: Agent = AgentSpecLoader().load_json(serialized_assistant)