Learn
Building a Search GPT with Dynamiq
Build a Search GPT that rephrases a question, searches the web and writes a cited answer, as a visual workflow called over HTTP or in Python with the SDK.

In short
A Search GPT answers questions from live web results and cites its sources. This tutorial builds one in three steps (rephrase the question, search the web, write a cited answer) two ways: as a visual workflow deployed as an API, and in Python with the open-source Dynamiq SDK. A small Streamlit app puts a search box in front of either backend.
A Search GPT is a question-answering app that searches the web for every query and writes a short answer with numbered citations. You can build one from three steps: an agent rewrites the question into a search query, a search tool fetches results, and a model writes the answer from those results only. This tutorial builds the same pipeline twice, as a visual workflow in Dynamiq that you call over HTTP and as Python code with the open-source SDK.
How does a Search GPT work?
Every question goes through the same three steps:
- Rephrase. Questions such as "what's going on with Apple stock lately?" make poor search queries. An agent turns them into short, keyword-focused queries, such as "Apple stock performance this week".
- Search. A search tool sends the query to a web search API and returns the top results with titles, snippets and URLs.
- Answer. A model writes a concise answer using only those results, cites them as [1], [2] and so on, and lists the sources.
Keeping the answer step grounded in the results is what separates a Search GPT from a chatbot answering from memory. If the results don't answer the question, the model should say so.
Approach 1: Build it visually and call it over HTTP
In Agent Builder, the pipeline is five nodes on the canvas:
- An Input node with an
inputfield for the question. - An Agent that rephrases the question, with the rephrasing instructions below as its role.
- A search tool, such as Scale SERP or Tavily, that takes the rephrased query.
- An LLM node that writes the cited answer, with the answer prompt below.
- An Output node that returns the answer.
Test it in the builder first. Each test run is traced, so you can see every node's input and output, how long it took and what it cost.
When the answers look right, deploy the workflow as an App. Every App gets its own hostname and accepts a POST request with an Access Key:
import os
import requests
ENDPOINT = "https://<your-app-hostname>" # shown on the App page
ACCESS_KEY = os.environ["DYNAMIQ_ACCESS_KEY"]
def ask(question: str) -> dict:
response = requests.post(
ENDPOINT,
headers={"Authorization": f"Bearer {ACCESS_KEY}"},
json={"input": {"input": question}, "stream": False}, # keys match your Input node
timeout=120,
)
response.raise_for_status()
return response.json() # your Output node's fields
Send "stream": true to receive the answer as server-sent events while it is generated. The HTTP reference for Apps covers streaming, async callbacks and error codes.
Approach 2: Build it in Python with the SDK
The same pipeline in code gives you full control over each step. Install the SDK with pip install dynamiq and set OPENAI_API_KEY and SERP_API_KEY (for Scale SERP).
First, the instructions for the two model steps. The answer prompt reads the original question from the workflow input and the search results from the search step.
REPHRASE_ROLE = """You turn user questions into effective web search queries.
Remove filler words, keep the essential keywords and names, and turn questions into short
statements. Examples:
- "Who was the first person to walk on the moon?" -> "First person to walk on the moon"
- "How tall is the Eiffel Tower?" -> "Eiffel Tower height"
Return only the rewritten query."""
ANSWER_PROMPT = """Answer the question using only the search results below.
Cite sources inline as [1], [2] and so on, matching a numbered source list.
If the results do not answer the question, say so and suggest a better query.
Question: {{ input }}
Search results:
{{ search_results }}
Return the answer inside <answer></answer> tags, followed by a numbered list of sources
formatted as markdown links inside <sources></sources> tags."""
Then the three nodes, wired into a workflow:
import re
from dynamiq import Workflow
from dynamiq.connections import OpenAI as OpenAIConnection
from dynamiq.connections import ScaleSerp as ScaleSerpConnection
from dynamiq.flows import Flow
from dynamiq.nodes.agents import Agent
from dynamiq.nodes.llms import OpenAI
from dynamiq.nodes.tools.scale_serp import ScaleSerpTool
from dynamiq.prompts import Message, Prompt
connection = OpenAIConnection()
rephraser = Agent(
id="rephraser",
name="rephraser",
role=REPHRASE_ROLE,
llm=OpenAI(connection=connection, model="gpt-4o-mini", temperature=0.1),
)
search = (
ScaleSerpTool(
id="search",
name="search",
connection=ScaleSerpConnection(), # reads SERP_API_KEY
limit=5,
is_optimized_for_agents=True, # returns results as readable text
)
.inputs(query=rephraser.outputs.content)
.depends_on(rephraser)
)
writer = (
OpenAI(
id="writer",
name="writer",
connection=connection,
model="gpt-4o",
temperature=0.2,
prompt=Prompt(messages=[Message(role="user", content=ANSWER_PROMPT)]),
)
.inputs(search_results=search.outputs.content)
.depends_on(search)
)
workflow = Workflow(flow=Flow(nodes=[rephraser, search, writer]))
def extract_tag(text: str, tag: str) -> str | None:
match = re.search(rf"<{tag}>(.*?)</{tag}>", text, re.DOTALL)
return match.group(1).strip() if match else None
def search_gpt(question: str) -> tuple[str | None, str | None]:
result = workflow.run(input_data={"input": question})
content = result.output[writer.id]["output"]["content"]
return extract_tag(content, "answer"), extract_tag(content, "sources")
.inputs() maps one node's output into the next node's input, and .depends_on() sets the order. The workflow's own input, the input question, is available to every node, which is how the answer prompt reads the original question.
A complete version of this project, with both backends and the frontend, lives in the SDK repository under examples/use_cases/search.
Add a Streamlit frontend
A few lines of Streamlit put a search box in front of either backend. Save the code above as search_backend.py, then run streamlit run app.py:
import streamlit as st
from search_backend import search_gpt
st.title("Search GPT")
question = st.text_input("Ask a question")
if st.button("Search") and question.strip():
with st.spinner("Searching..."):
answer, sources = search_gpt(question)
st.markdown(answer or "No answer found. Try rephrasing the question.")
if sources:
st.markdown("**Sources**")
st.markdown(sources)
To show the answer as it is written, enable streaming on the writer node and read the chunks with a streaming callback handler; the streaming guide shows the pattern.
How do you make a Search GPT reliable enough for work?
A demo answers most questions well. A tool people rely on needs a few more controls:
- Restrict the sources. For research in regulated fields, search only sources you trust.
TavilyToolaccepts a list of domains to include, such as regulator and central bank sites. - Add your own documents. Combine web search with a knowledge base of internal documents, so answers can draw on both.
- Check for prompt injection. Web pages can contain instructions aimed at models. A guardrail node can check search results for prompt injection before the writer sees them.
- Measure answer quality. Score answers for faithfulness to the sources with evaluations, and turn bad answers from production traces into test cases.
FAQ
What is a Search GPT?
A Search GPT is an application that answers questions by searching the web in real time and writing a short answer with citations to the pages it used. It combines a search API with a language model, so answers reflect current information rather than only what the model learned in training.
Which search API should I use?
The Dynamiq SDK includes tools for Tavily, Exa, Scale SERP, Firecrawl and Jina search. They differ in result format, freshness and filtering options, so test two or three on your own questions before you choose.
Why rephrase the question before searching?
Search engines work best with short, keyword-focused queries. Rewriting a conversational question into a search query usually returns more relevant results, which leads to better answers.
How do you stop the answer from making things up?
Tell the model to answer only from the search results, require a citation for each claim, and have it say so when the results don't contain the answer. Then measure faithfulness with evaluations and review low-scoring answers.
Can a Search GPT search internal documents too?
Yes. Add a retrieval tool over a knowledge base of internal documents alongside web search, or instead of it, so the same pipeline answers questions about your own policies, products or research.



