Writing a custom deep researcher

A deep researcher is an ordinary AI tool — same editor, same access modes, same versioning, same tool picker — whose code returns an object implementing IDeepResearcher instead of a class with [Tool] methods. That single difference is the whole of it: there are no methods to reflect over, so the tool surface handed to the model is synthesized, and a call of it starts a research run instead of invoking one of your methods.

You describe how the research should be done. The loop itself belongs to the workspace.

Start from the web researcher

Settings → AI → AI Tools → New deep research tool creates a tool seeded with the web researcher below and opens its editor. It is the same flow as "New AI Tool", only the starting point is a researcher rather than a search tool.

It gathers through the sample Search the Web tool, which calls the Brave Search API — so before it can do anything, either open that tool and put your key in it (get one here), or point ResearchTools at a web search tool you already have.

// A deep researcher is an AI tool whose code returns an IDeepResearcher instead of a class
// with [Tool] methods. It does not do the research itself: it says which prompt drives each
// phase of the research loop, which tools the loop may call while gathering evidence, and
// how much a single run may spend.
[ToolSystemPrompt("""
                  A deep research run takes minutes and the workspace runs only a few at once. Start one
                  when the question needs several web searches weighed against each other; answer from a
                  single search when a single search would do, and say what you are about to do before
                  starting a run.
                  The research answers with a written, sourced report. Give the user that report — keep
                  its structure and its links rather than summarising it into a paragraph — and do not
                  add claims of your own to it. While a research is running the conversation accepts no
                  further messages, so do not ask the user anything you need the answer to first.
                  """)]
public class WebDeepResearcher : IDeepResearcher
{
    // What the assistant reads when it decides whether a question deserves a research run.
    public string Description => "Researches a question in depth on the public web: plans the question into steps, runs web searches across them, reads what they turn up, and answers with a written, sourced report. Use it for questions that need several searches and a comparison of what they return, not for a single lookup.";

    // The six phases of the loop.
    public PromptTemplateUID PlanPrompt       => new PromptTemplateUID(BuiltInUIDs.PromptTemplate_DeepResearchPlan);
    public PromptTemplateUID QueriesPrompt    => new PromptTemplateUID(BuiltInUIDs.PromptTemplate_DeepResearchQueries);
    public PromptTemplateUID SearchPrompt     => new PromptTemplateUID(BuiltInUIDs.PromptTemplate_DeepResearchSearch);
    public PromptTemplateUID FindingsPrompt   => new PromptTemplateUID(BuiltInUIDs.PromptTemplate_DeepResearchFindings);
    public PromptTemplateUID DirectionsPrompt => new PromptTemplateUID(BuiltInUIDs.PromptTemplate_DeepResearchDirections);
    public PromptTemplateUID ReportPrompt     => new PromptTemplateUID(BuiltInUIDs.PromptTemplate_DeepResearchReport);

    // What the gather phase may call. Add the workspace `search` and `consult` tools here as
    // well if you want a run to weigh what the workspace knows against what the web says.
    public ToolUID[] ResearchTools => new[]
    {
        new ToolUID(BuiltInUIDs.Tool_SampleWebSearch),
    };

    // Budgets for a single run. Zero means "use the workspace default"; the workspace's own
    // ceiling (Deep Research, under AI in the admin settings) always wins.
    public int MaxIterations       => 12;   // directions explored across the whole run
    public int MaxDurationSeconds  => 600;  // wall clock for the whole run
    public int QueriesPerDirection => 3;    // searches generated per direction
}

return new WebDeepResearcher();

The seeded Workspace Deep Research tool is the same class with a different Description and ResearchToolsTool_LLMSearch plus Tool_Consult, so the run searches the workspace and reads what it finds.

What a researcher declares

Member What it is for
Description Written for the model that decides whether to call the tool. Say which material it researches and when a question deserves a full run rather than a single search. It becomes the description of the synthesized tool.
PlanPrompt Turns the question into the plan: ordered steps, each with its first directions. Answers with the plan JSON.
QueriesPrompt Turns one direction into the queries to run. Answers with the queries JSON.
SearchPrompt Drives the one phase that may call tools: it is handed the queries and your tools, and reports what it found in prose.
FindingsPrompt Distils what the gather phase collected into discrete findings with their sources. Answers with the findings JSON.
DirectionsPrompt Decides what is still worth exploring in the current step. Answers with the directions JSON.
ReportPrompt Writes the final markdown report the assistant answers from.
ResearchTools The AI tools the gather phase may call.
MaxIterations Directions the whole run may explore. 0 uses the workspace default.
MaxDurationSeconds Wall clock for the whole run. 0 uses the workspace default.
QueriesPerDirection Queries generated per direction. 0 uses the built-in default of 3; the loop caps it at 6.

Which placeholders each phase receives is listed in Deep research → Each phase is an editable prompt. A prompt you point at is expected to answer in the shape the loop parses — read the built-in template before replacing one.

Pointing at tools of your own

ResearchTools is what makes a researcher yours: it decides what the run can find at all. Use the generated AI_Tools helper rather than hardcoding UIDs — it is filled in from the workspace's own tools, so a typo is a compile error:

public ToolUID[] ResearchTools => new[]
{
    AI_Tools.SearchTickets,
    AI_Tools.ReadTicket,
    new ToolUID(BuiltInUIDs.Tool_LLMSearch),
};

Two rules the loop enforces:

  • A search tool alone is not enough. Pair something that finds items with something that reads one in full — a finding is only worth citing when it came from something that was actually read. That is why the workspace researcher pairs search with consult.
  • The caller's access is enforced on every call. Tools are resolved against the user who asked before the run starts; a researcher can never widen what its caller may read. A researcher whose tools are all inaccessible to that user fails with an explanation instead of researching with fewer tools.

Keep the list short. Every extra tool is another thing the gather phase has to choose between on every direction.

Giving the researcher its own voice

The six built-in templates are shared: editing one under Settings → AI → Prompt Templates changes every researcher pointing at it. To change one phase for one researcher, copy that template into a new one and point the matching property at your copy — again through the generated helper:

public PromptTemplateUID PlanPrompt   => PromptTemplates.TicketResearchPlan;
public PromptTemplateUID ReportPrompt => PromptTemplates.TicketResearchReport;
// the other four keep the built-ins

This is the right lever for domain vocabulary ("a direction here means one product line"), for the shape of the report, and for anything the model would otherwise have to guess about your data.

Say what has to hold for the conversation

[ToolSystemPrompt] goes on the researcher class, and its excerpt is appended to the system prompt of every chat or agent run that offers the tool — see Built-in AI Tools.

Both shipped researchers use it for the three things an assistant otherwise gets wrong:

  1. a run is expensive, so start one only when a single search would not do — and say so before starting;
  2. the report is the answer: hand it over with its structure and its sources, do not summarise it into a paragraph and do not add claims to it;
  3. the conversation takes no messages while a run is in flight, so do not ask the user a question whose answer you need first.

Keep your own excerpt in the same register: short, about this tool, and true for the whole conversation. It is paid for on every turn of every chat the tool is enabled in.

Trying it

  1. Enable the tool on an assistant, or pick it in a chat's tool picker.
  2. Ask something that needs research. The assistant decides to call it, and the plan card appears inline as soon as the run is queued.
  3. Watch Settings → AI → Deep Research while it runs — the researcher is listed there with its budget, and the run appears under what is executing now.

Common first failures:

Symptom Cause
The assistant answers from a single search instead of researching The Description does not say when a question deserves a run — it is the only thing the model reads when deciding.
The run starts and immediately fails No research tool is accessible to the calling user, or ResearchTools is empty.
The report has no sources The gather phase had nothing that reads an item in full — pair the search tool with a reading tool.
Runs are refused The shared queue is full. The limits are on the Deep Research admin page.
© 2026 Curiosity. All rights reserved.