Orchestrating deep research with Temporal, OpenAI, and Box AI

|
Share

Imagine an international film distributor is preparing the release of an independent film in four markets. The team needs to understand how audiences discover films in each country before deciding where to invest in festivals, theatrical partnerships, streaming promotion, critics, creators, or social campaigns.

The research brief looks like this:

Research current local-language industry publications, national film institute reports, festival programs, cinema association data, and platform research to compare how audiences in France, Japan, Brazil, and Poland discover independent films. Assess the roles of festivals, theatrical release, streaming platforms, critics, creators, and social communities. The final Box AI brief must compare this external evidence with the internal Box context selected for the project — either explicit files or a curated Box Hub — while clearly distinguishing public findings from internal context.

The useful evidence for this case is distributed across different languages and information ecosystems. France has its own film institutions and festival network. Japan has local trade publications and discovery platforms. Brazil's creator and community landscape is discussed in Portuguese. Polish audience research and industry coverage may never appear near the top of an English-language search.

The multilingual angle is therefore part of retrieval, not just presentation. If the system searches only in English and translates the final answer, it can produce polished briefs while missing the local evidence that should shape them.

That’s the challenge behind this demo project: turn one cross-market question into a durable research process that searches current external sources, preserves citations, produces localized briefs, and publishes a reusable research pack to a Box folder. Once the external report is in Box, Box AI compares it with internal files that have been selected for the project and produces a cited decision brief without downloading those internal documents into the application.

Defining workflow building blocks

This demo research workflow includes several distinct steps:

1. Turning the business question into a research plan.

2. Generating focused queries for each market and language.

3. Searching current external sources.

4. Preserving useful results when one search doesn’t complete.

5. Synthesizing evidence without hiding contradictions or gaps.

6. Creating localized briefs from the same source report.

7. Uploading every artifact without leaving duplicate files behind during retries.

8. Asking Box AI to compare the report with explicitly selected internal Box files.

Some operations can run concurrently. Others need outputs created by earlier steps. Every model call, search, and upload crosses a network boundary. The approach changes from a prompt to a distributed workflow with state and side effects.

The project assigns a clear responsibility to each system:

  • OpenAI plans, searches the web, synthesizes evidence, and creates localized briefs.
  • Temporal stores execution state and coordinates dependencies, concurrency, retries, and failure semantics.
  • Box stores the source research information and market briefs as managed content. Those files are versioned, and are governed with a secure permission layer. Beyond this demo implementation, it also provides an additional notification layer for humans, who can monitor changes done by the agent. Box AI queries deliberately selected internal content server-side and returns a cited decision brief grounded in the permissions of the authenticated Box user.

Following one durable Workflow execution

The implementation is adapted from Temporal's OpenAI deep-research cookbook example. The main Workflow reads like asynchronous Python:

@workflow.defn
class DeepResearchWorkflow:
	@workflow.run
	async def run(self, request: ResearchRequest) -> GlobalSignalDeskResult:
    	# 1. Plan the investigation.
    	research_plan = await plan_research(request.query)
 
    	# 2. Create focused search queries.
    	query_plan = await generate_queries(research_plan, request.research_languages)
 
    	# 3. Run searches concurrently and keep useful partial results.
    	search_results = await self._execute_searches(query_plan.queries)
    	if not search_results:
        	raise ApplicationError(
            	"No web searches produced usable results",
            	"NO_SEARCH_RESULTS",
            	non_retryable=True,
        	)
 
    	# 4. Synthesize the cited master report.
    	final_report = await generate_synthesis(
        	request.query,
        	research_plan,
        	search_results,
    	)
    	formatted_report = self._format_final_report(request, final_report)

This class describes high level flow and dependencies. That’s fundamental to Temporal: a Workflow contains deterministic orchestration. On the contrary, a Temporal Activity performs non-deterministic work such as API calls and external writes.

Additionally, Temporal records the Workflow's progress in Event History. If a Worker process restarts, the Workflow is replayed against that history to reconstruct its state. Results from completed Activities are already recorded, so the orchestration can resume without deliberately repeating every successful model call.

For an agent, this history is effectively the durable record of what has happened: the plan was created, these searches completed, synthesis received these results, and publication produced these Box file IDs. Durability here means that the process around the model has a recoverable state.

Planning multilingual retrieval

The query-generation step receives both the research plan and the required research languages. What’s crucial is that it creates searches that use the vocabulary and source landscape of each market.

request = ResearchRequest(
    query=research_question,
	research_languages=[
    	"French",
    	"Japanese",
    	"Portuguese",
    	"Polish",
	],
	output_languages=[
    	"English",
    	"French",
    	"Japanese",
    	"Portuguese",
    	"Polish",
	],
	...
)

research_languages controls evidence collection. output_languages controls the briefs created after synthesis. Keeping them separate prevents presentation choices from narrowing the source base. For the film-discovery brief use case, useful queries might target various :

●      French film institute research and festival attendance,

●      Japanese independent cinema and local streaming discovery,

●      Brazilian audiovisual market studies and creator communities,

●      Polish film festivals, cinema, and audience reports.

Image of Python coding

This way, each query becomes structured input to an OpenAI web-search Activity:

result = await workflow.execute_activity(
	invoke_model,
	InvokeModelRequest(
        model=EFFICIENT_PROCESSING_MODEL,
        instructions=WEB_SEARCH_INSTRUCTIONS,
    	input=search_input,
        response_format=SearchResult,
    	tools=[{"type": "web_search"}],
	),
    start_to_close_timeout=timedelta(seconds=300),
	summary="Searching web for information",
)

SearchResult is a Pydantic model rather than a raw block of text and it carries the query that was executed, source URLs and descriptions, relevant findings, and formatted citations. Those typed results matter because every stage has a contract. The synthesis step should receive evidence and citations, not reverse-engineer them from text artifacts generated by the previous call. The Temporal client and Worker use the Pydantic data converter to move those objects across the Workflow boundary.

Parallel searches without losing useful work

Next, market searches are executed independently, so the Workflow starts them out concurrently:

async def _execute_searches(self, search_queries) -> list[SearchResult]:
    	async def execute_single_search(search_query):
        	try:
            	return await search_web(search_query)
        	except Exception:
            	workflow.logger.exception(
                	"Search did not complete for query %s",
                	search_query.query,
            	)
            	return None
 
    	results = await asyncio.gather(*(execute_single_search(query) for query in search_queries))
    	return [result for result in results if result is not None]

The pattern is fan-out followed by fan-in: searches can progress independently, while synthesis waits for the available results. A terminal failure in one search doesn’t discard successful evidence from every other market. The Workflow can continue with useful partial results. If no search produces evidence, it stops with a non-retryable NO_SEARCH_RESULTS error instead of asking the model to invent a report from an empty context.

This is a more useful definition of agent durability than “retry everything.” Durable execution preserves completed work and makes the conditions for continuing explicit.

Synthesis and localization share one evidence base

Once multilingual search completes, the synthesis Activity receives the original question, research plan, and all usable SearchResult objects. It creates a master report with an executive summary, detailed cross-market analysis, key findings, citations and more. Localization happens after synthesis:

localized_briefs = await asyncio.gather(
	*(
        generate_localized_brief(
        	request.query,
        	final_report,
        	language,
    	)
    	for language in request.output_languages
	)
)

All briefs derive from the same cited report. The language and emphasis can be localized; the underlying facts, uncertainty, and citations remain anchored to one master artifact.

Making Box publication retry-safe

Temporal may execute an Activity more than once, so uploading a file must be safe to repeat. Every output filename contains the stable Temporal Workflow ID. Before uploading, the Box Activity checks the destination folder for an exact filename. A repeated upload therefore creates a new version of the intended file rather than another copy with a slightly different name:

existing = _existing_file(
	client,
	folder_id,
	artifact.filename,
)
 
if existing:
	updated = client.uploads.upload_file_version(
    	existing.id,
        UploadFileVersionAttributes(
            name=artifact.filename
    	),
    	content,
	)
	entry = updated.entries[0]
else:
	uploaded = client.uploads.upload_file(
        UploadFileAttributes(
            name=artifact.filename,
            parent=UploadFileAttributesParentField(
            	id=folder_id
        	),
    	),
    	content,
	)
	entry = uploaded.entries[0]

Temporal provides durable retries, but the application still has to make external side effects safe to repeat. Stable Workflow IDs provide the correlation key; Box file versioning provides the duplicate-resistant write behavior.

This distinction matters; the implementation is retry-safe, but isn’t strictly idempotent. Each retry after a successful-but-unacknowledged upload can add another file version and a trace for humans to review and follow the updates made by the agent. A production implementation should prevent Workflow ID reuse and persist the returned Box file ID, so later attempts can address the object directly.

The completed research pack for this demo contains:

market-reaserch-workflow-id--master-report.md
market-reaserch-...--brief-english.md
market-reaserch-...--brief-french.md
market-reaserch-...--brief-japanese.md
market-reaserch-...--brief-portuguese.md
market-reaserch-...--brief-polish.md
market-reaserch-...--sources.json
market-reaserch-...--box-ai-digest.md

The source manifest keeps external references machine-readable. The master report remains the shared evidence base.

Enhancing the research with internal context with Box AI

The workflow could stop after uploading the externally researched report. Instead, Temporal passes its new Box file ID, the original business question, and the explicitly configured internal file IDs to another Activity:

def _box_ai_request(
	request: BoxAIReviewRequest,
) -> tuple[CreateAiAskMode, str, list[AiItemAsk]]:
	prompt_parts = [
    	"BUSINESS RESEARCH QUESTION:\n"
    	+ _bounded_text(request.research_question, 2_000),
        BOX_AI_DIGEST_PROMPT,
	]
 
	if request.internal_hub_id:
        prompt_parts.extend(
        	[
                BOX_AI_HUB_CONTEXT_PROMPT,
            	"EXTERNAL RESEARCH REPORT:\n"
            	+ _bounded_text(request.report_text, 5_000),
        	]
    	)
    	return (
            CreateAiAskMode.SINGLE_ITEM_QA,
        	"\n\n".join(prompt_parts),
        	[
            	AiItemAsk(
                	id=request.internal_hub_id,
                	type=AiItemAskTypeField.HUBS,
            	)
        	],
    	)
 
	return (
        CreateAiAskMode.MULTIPLE_ITEM_QA,
    	"\n\n".join(prompt_parts),
    	[
        	AiItemAsk(
            	id=file_id,
            	type=AiItemAskTypeField.FILE,
        	)
        	for file_id in [
                request.report_file_id,
                *request.internal_file_ids,
        	]
    	],
	)
 
 
def _analyze_with_box_ai_sync(
	request: BoxAIReviewRequest,
) -> BoxAIDigest:
	client = _box_client()
	mode, prompt, items = _box_ai_request(request)
 
	response = client.ai.create_ai_ask(
    	mode,
    	prompt,
    	items,
        include_citations=True,
	)
 
	payload = response.to_dict()
 
	return BoxAIDigest(
        answer=_coerce_answer(
        	payload.get("answer", {})
    	),
        citations=payload.get("citations") or [],
        completion_reason=payload.get(
        	"completion_reason"
    	),
        created_at=payload.get("created_at"),
	)

Script accepts one generated report plus up to 24 internal files, matching the 25-file limit for Box AI multi-document Q&A. It doesn’t discover internal documents, broaden permissions, or create substitute content. The file IDs must refer to real documents deliberately selected by the team, and the Box service account or an authenticated user used by the Worker must already have access to them. Alternatively, this could be a Box Hub AI query, with access to extensive content with one single API call.

Image of Python coding

This step is not a replacement for external research. OpenAI searched current web sources and produced the report. Box AI operates on the completed artifact, with access to internal proprietary context that can be added to the agent. This way it can provide curated briefs that are tailored to specific campaigns that are planned.

The research pipeline optimizes for evidence collection and synthesis and Box AI optimizes for turning generated content into actionable insights for this business initiative.

Image of Box AI

Try it yourself

If you want to try running this agent yourself, the code is open source and the the demo requires:

●      Python 3.11 or newer and `uv`

●      Temporal CLI installed

●      An OpenAI API key with access to the configured models and web search

●   A Box free developer account with a Box Platform App with read, write, and AI scopes

●      A Box folder for the generated research pack, one or more existing internal Box files selected for the final comparison, and application service account that’s collaborated to this content

Clone the repository, install packages, copy the .env.example file and provide your credentials:

# OpenAI Responses API
OPENAI_API_KEY=
OPENAI_COMPLEX_MODEL=gpt-5.6-sol
OPENAI_FAST_MODEL=gpt-5.6-terra
 
# Box demo authentication (developer tokens are short-lived)
BOX_DEVELOPER_TOKEN=
BOX_FOLDER_ID=
# Comma-separated IDs for 1-24 internal Box files used by the final Box AI review
BOX_INTERNAL_FILE_IDS=
BOX_INTERNAL_HUB_ID=
 
# Local Temporal defaults
TEMPORAL_ADDRESS=localhost:7233
TEMPORAL_NAMESPACE=default
TEMPORAL_TASK_QUEUE=global-signal-desk

Start the persistent local Temporal server:

make temporal

Start the Worker in a second terminal:

uv run python -m worker

In third terminal run the research brief:

uv run python -m start_workflow

Temporal Web shows the complete Activity timeline and Box receives the master report, source manifest, localized briefs, and the Box AI brief.

Disclaimer: this is a demo implementation, so several production decisions remain intentionally simple. The Box developer token is convenient but short-lived. A deployed worker should use an enterprise-approved OAuth, JWT, or Client Credentials Grant flow.

Final thoughts

The important output of this demo isn’t simply another generated report. It’s a research process with explicit stages and recoverable state: multilingual retrieval, typed evidence, partial-result handling, shared synthesis, retry-safe publication, and a final comparison against governed internal context.

That distinction matters in enterprise AI. Models can plan, search, and synthesize, but a useful agent must also survive delays and failures, preserve completed work, respect content permissions, and leave evidence that people can inspect. Temporal makes the process durable. OpenAI brings current external signals into it. Box and Box AI turn the result into managed, organization-specific knowledge.