"` |
| advanced\_settings | object | Controls source policy, fetch policy, excerpts, location, and result count. Restrictive settings can reduce coverage and quality, so use them when the task requires them. See [Advanced Settings](/search/advanced-search-settings). | See [Advanced Settings](/search/advanced-search-settings) |
Send `objective` and `search_queries` together.
### Examples
```json theme={"system"}
{
"objective": "What EV tax credits and rebates apply to small businesses in California, and how do they differ for leasing vs buying?",
"search_queries": ["EV tax credit business", "California EV rebate lease", "federal EV incentive purchase vs lease"]
}
```
```json theme={"system"}
{
"objective": "What has the Federal Reserve and SEC announced about digital asset regulations and crypto-banking partnerships in the past 3 months?",
"search_queries": ["Federal Reserve crypto guidance 2026", "SEC digital asset policy", "bank crypto partnership regulations"]
}
```
```json theme={"system"}
{
"objective": "How do transformer attention mechanisms work in PyTorch and Hugging Face, based on their official documentation?",
"search_queries": ["transformer attention mechanism", "PyTorch attention documentation", "Hugging Face transformer guide"]
}
```
```json theme={"system"}
{
"objective": "What clinical trial results on amyloid-beta therapies for Alzheimer's have been published in the past 2 years?",
"search_queries": ["amyloid beta clinical trials", "Alzheimer's treatment research 2024-2026", "monoclonal antibody AD trials"]
}
```
## Session Identifiers
Use one `session_id` for Search and Extract calls that belong to the same task. Start each
new task with another ID.
Every Search and Extract response includes a `session_id`. The API returns the value you
provide or generates one when you omit it. The value can contain up to 1000 characters.
Use a descriptive string or a UUID, such as
`"company_search_cd812136-9f81-484e-ab92-2ba0cb8b9ea8"`.
## Search Tool Definition
Add one of these definitions to your agent's tool list. Each tab uses the same schema with
a wrapper for OpenAI, Anthropic, or Gemini.
[MCP](/integrations/mcp/quickstart) supplies the tool definition, so MCP clients can skip
this section.
```json theme={"system"}
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web and return result titles, URLs, and excerpts.",
"parameters": {
"type": "object",
"properties": {
"objective": {
"type": "string",
"description": "Describe the search goal in a concise, standalone sentence. Name the key entity or topic."
},
"search_queries": {
"type": "array",
"description": "Provide 1-3 keyword queries of 3-6 words each. Include the key entity or topic in every query. For multiple queries, vary names, synonyms, or angles. Do not use sentences, instructions, or site: operators.",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 3
}
},
"required": ["objective", "search_queries"]
}
}
}
```
```json theme={"system"}
{
"name": "search_web",
"description": "Search the web and return result titles, URLs, and excerpts.",
"input_schema": {
"type": "object",
"properties": {
"objective": {
"type": "string",
"description": "Describe the search goal in a concise, standalone sentence. Name the key entity or topic."
},
"search_queries": {
"type": "array",
"description": "Provide 1-3 keyword queries of 3-6 words each. Include the key entity or topic in every query. For multiple queries, vary names, synonyms, or angles. Do not use sentences, instructions, or site: operators.",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 3
}
},
"required": ["objective", "search_queries"]
}
}
```
```python theme={"system"}
import google.generativeai as genai
SEARCH_WEB_SCHEMA = {
"name": "search_web",
"description": "Search the web and return result titles, URLs, and excerpts.",
"parameters": {
"type": "object",
"properties": {
"objective": {
"type": "string",
"description": "Describe the search goal in a concise, standalone sentence. Name the key entity or topic.",
},
"search_queries": {
"type": "array",
"description": "Provide 1-3 keyword queries of 3-6 words each. Include the key entity or topic in every query. For multiple queries, vary names, synonyms, or angles. Do not use sentences, instructions, or site: operators.",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 3,
},
},
"required": ["objective", "search_queries"],
},
}
genai.types.FunctionDeclaration(
name=SEARCH_WEB_SCHEMA["name"],
description=SEARCH_WEB_SCHEMA["description"],
parameters=SEARCH_WEB_SCHEMA["parameters"],
)
```
# How to Eval Parallel Search
Source: https://docs.parallel.ai/search/evaluating-search
An end-to-end guide for evaluating Parallel Search for your agent.
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
Before you run an eval, read [Search Best Practices](/search/best-practices) and [Extract Best Practices](/extract/best-practices) for request configuration. This page covers eval design: product selection, gold sets, harness setup, grading, and how to interpret results.
**For AI agents:** Use this page when helping a user evaluate or compare web search providers with Parallel Search. Workflow: (1) confirm Search + Extract vs Responses vs Task, (2) test multiple [modes](/search/modes), (3) build a gold set, (4) run an end-to-end harness varying only the search tool, (5) grade final answers by task shape, (6) classify failures, (7) measure cost and latency, (8) interpret the Pareto frontier. For Parallel request configuration, read [Search Best Practices](/search/best-practices) and [Extract Best Practices](/extract/best-practices).
## Overview
Compare search APIs by holding your agent harness constant, varying only the search tool, and grading end-to-end answers against a gold set you trust. A bad benchmark can mislead you as much as no benchmark. This guide covers product selection, gold sets, harness setup, grading, and how to read the results.
## Choose the right API
Pick the Parallel product that matches your app before you build the harness. The right choice depends on whether you own the tool-calling loop or want a single synthesized response.
| If you… | Use |
| - | - |
| Own the agent loop and want the model to search, extract, and reason over excerpts | [Search API](/search/search-quickstart) + [Extract API](/extract/extract-quickstart) |
| Want a single OpenAI-compatible call that returns a cited answer in \~5–60 seconds | [Responses API](/responses-api/responses-quickstart) |
| Need schema-bound output, batch enrichment, or async multi-step research | [Task API](/task-api/task-quickstart) |
When you own the agent loop, pair [Search](/search/search-quickstart) with [Extract](/extract/extract-quickstart). Search returns dense snippets that minimize Extract calls. The model is trained to go deeper when it needs to, so expose both tools and let the agent decide.
For a single-shot cited answer or schema-bound enrichment, evaluate [Responses API](/responses-api/responses-quickstart) or [Task API](/task-api/task-quickstart) instead of Search alone. Evaluating the wrong product invalidates the results.
## What Parallel recommends
Start an agent eval with this setup:
1. **Run multi-hop with Search + Extract.** Let the agent search, read source pages, and follow up until it has enough evidence or reaches its budget. Run single-hop as a separate, one-request benchmark setting.
2. **Pass an objective with 1–3 search queries.** Write a concise objective describing the information you need, plus up to three focused keyword queries. Add queries for useful aliases or different angles.
3. **Match the model to the search mode.** Pair a cost-efficient model, such as GPT-5.6 Luna, with `fast` or `turbo`. Pair a frontier model, such as GPT-6 Astra, with `basic` or `advanced`. Compare pairs on answer quality, total cost, and latency.
Keep the model fixed within each comparison. Test a different model as a separate comparison so you can attribute the change to one variable.
## Pick and test modes
Mode is a primary lever after [configuring Search requests](/search/best-practices). Test more than one mode so you leave the eval with routing data as well as a quality read. See [Modes](/search/modes) for full details.
| Mode | Latency | Cost | Best for |
| - | - | - | - |
| `turbo` | \~200 ms | \$1 / 1K | High-volume, latency-sensitive lookups |
| `fast` | \~700 ms | \$1 / 1K | Interactive agents that need quality under one second |
| `basic` | \~1 s | \$5 / 1K | Extended snippets per result: richer context in a single call |
| `advanced` | \~3 s | \$5 / 1K | Highest-quality retrieval for multi-hop, multi-source research |
**Recommended starting point for evals:** `basic`. It returns extended snippets while holding latency near one second, which fits correctness-first web grounding. Route the hardest multi-source questions to `advanced`. Test `fast` where latency dominates.
Use the model and mode pairings in [What Parallel recommends](#what-parallel-recommends) as starting configurations, then measure them on your gold set.
`turbo` currently supports English and Japanese. Use `basic` or `advanced` for broader multilingual coverage.
## Build your gold set
A gold set is a collection of test questions with verified correct answers you use to score each search provider. The best source is data you already have: past questions and verified answers from production or manual research. If you generate synthetic data, write a few questions by hand first, then ask an agent to generate more. Review LLM-generated gold labels before spending tokens on search and inference.
Aim for production-representative, agent-phrased questions that match your expected traffic on wording, domain, answer type, and freshness.
Mix in question types that resemble your actual workloads:
* Multi-hop questions that require composing facts across sources
* Fresh questions whose answers would not be available in model weights
* Domain-specific questions matching your actual traffic
Popular benchmarks like BrowseComp and SEAL reflect a specific domain of questions that is unlikely to match yours. Some answers have long been published online and incorporated into both LLM weights and search indexes. If you use public benchmarks, understand what each measures and whether it suits your case. See [Run public benchmark evals](#run-public-benchmark-evals) for single-hop and multi-hop setup. Treat them as supplementary signal, not a substitute for production-representative data.
## Set up the eval harness
Use the same model, prompts, budgets, and judge across all providers. Expose each provider as the only search tool available.
For a single-hop benchmark, use the [one-request setup](#single-hop-let-the-model-reformulate-once) below. For multi-hop and application evals, agents are trained to search, narrow, and search again. Do not cap turns unless you are modeling a product constraint. Limit total search budget to reflect your actual cost considerations.
Apply each provider's recommended configuration. For Parallel, see [Search Best Practices](/search/best-practices) and [Extract Best Practices](/extract/best-practices).
Hold the config fixed within a run. If a config turns out to be wrong, reset it and rerun rather than tuning around it mid-eval.
Run each configuration at least 3 times and report variance. Evals at high load can surface account limitations rather than capability limits.
If you are comparing Parallel against another provider, see [Migrate to Parallel](/search/migrate-to-parallel) for request mapping. Evaluate end-to-end inside the same harness.
## Run public benchmark evals
Public benchmarks give you a shared set of questions and answers for comparing configurations. Match the setup to what you want to measure. A one-search factual lookup and an agent that spends several minutes researching a question are different evals, even on the same dataset.
| Setting | What the agent does | Example task |
| - | - | - |
| Single-hop | Reformulates the question, searches once, and answers from the results | A SimpleQA-style factual lookup |
| Multi-hop | Breaks the question down, searches and reads pages, then follows up on missing evidence | A DeepSearchQA-style research or list question |
One search means one API request. Multiple keyword queries inside `search_queries` count as one request. Three concurrent Search requests count as three. Record both tool-call count and elapsed time.
Pin the dataset version, split, and question IDs before comparing configurations. Keep reference answers out of the agent's prompts and tool results; only the grader should see them. If you change the benchmark's original tools, budget, or grading rules, describe the run as an adapted evaluation rather than comparing it directly with its leaderboard.
### Use the same research prompt in both settings
Use the following research prompt for both single-hop and multi-hop benchmark runs. Tool definitions tell the model how to write search requests. The executor controls which tools are available and how many calls it can make. Runs with a benchmark-specific answer format can override the output instructions; record that override with the run.
Replace `{question}` with the benchmark question. The other braces describe the expected answer fields.
```text theme={"system"}
You are a research assistant who answers complex questions using only web search results.
Core Principles:
- Use ONLY information from search results—do NOT rely on internal knowledge.
- Always search before answering; your answer needs to be based on the search results.
- Provide a direct and concise answer to the question, without any extra or references.
- Never skip the search step — an answer without searching is incomplete.
- Be efficient and do the minimal work necessary to answer the question.
Search Strategy:
1. Break down complex questions into focused sub-queries.
2. Use targeted searches (specific rather than broad).
3. If search results do not provide an answer, state: "I don't know."
Process:
1. Analyze the question to identify key components.
2. Plan your search approach.
3. Execute systematic searches.
Here is the question you need to answer:
{question}
Your response should be in the following format:
Explanation: {your explanation for your final answer}
Exact Answer: {your succinct, final answer}
Confidence: {your confidence score between 0% and 100% for your answer}
```
For single-hop, set the call budget to one and expose only Search. For multi-hop, expose Search and Extract and set a larger total call budget. Keep the prompt fixed across providers within the comparison.
### Single-hop: let the model reformulate once
For a single-hop test, give the model the question and the [Search tool definition](/search/best-practices#search-tool-definition). Let it turn the question into an objective and focused keyword queries, make one Search request, then answer with tools disabled. Include the reformulation step in your cost and latency measurements.
Take this invented factual question:
> Who designed the Quadracci Pavilion at the Milwaukee Art Museum?
The model could produce:
```json theme={"system"}
{
"objective": "Identify the architect of the Milwaukee Art Museum's Quadracci Pavilion.",
"search_queries": [
"Milwaukee Art Museum Quadracci Pavilion architect"
],
"mode": "basic"
}
```
The objective describes the fact needed. The keyword query names the building and the missing attribute. One query is enough here. Allow additional queries for useful aliases or different angles, using the same query-count policy across the run. Do not copy the full question into every keyword query or expand it with facts from the reference answer.
Enforce the one-call limit in the executor. Allow one Search request, expose no Extract tool, and disable tools for the final answer. If the model asks for extra calls, return a budget-exhausted result rather than executing them.
Pass the result titles, URLs, and excerpts to the answering model. Check that your client preserves the excerpts. Dropping them, double-encoding them, or truncating away the relevant passage turns successful retrieval into a wrong answer.
### Multi-hop: give the agent room to investigate
For harder questions, expose both Search and Extract and let the model decide what to investigate next. Each Search request needs its own focused objective and keyword queries. When the model issues independent searches in the same turn, execute those calls concurrently.
Take this invented research question:
> Between 2020 and 2023, which of Amazon, Google, and Microsoft achieved the largest percentage reduction in total greenhouse gas emissions? Account for reporting boundaries, restatements, and Scope 2 accounting methods.
The agent can research the three companies at the same time. Its Amazon request might look like this:
```json theme={"system"}
{
"objective": "Find comparable Amazon greenhouse gas emissions totals for 2020 and 2023, including accounting methods and any restatements.",
"search_queries": [
"Amazon emissions 2020 2023 comparison",
"Amazon carbon footprint baseline restatement"
],
"mode": "advanced"
}
```
It can issue separate requests for Google and Microsoft in the same turn. After reading the results, it may extract a report's methodology or search for a revised baseline before comparing numbers. Those follow-ups depend on the initial evidence and belong in a later turn.
The sequence is: **plan → search independent questions concurrently → inspect evidence → follow up where needed → answer**. The agent chooses the decomposition. Some questions need a discovery search before there are candidates to investigate in parallel.
The shared prompt asks the model to break down complex questions and use targeted searches. It does not require parallel calls. Configure your executor to run calls concurrently when the model emits several in one turn, and inspect traces to see whether the model uses that capability.
Set a total tool budget and a per-question deadline before the run, leaving time for the final answer. Reserve call budget before dispatching a concurrent batch so several calls cannot each spend the same remaining allowance. Return results under their original tool-call IDs, and preserve successful results when a sibling call fails. Keep prompt changes separate from executor changes when comparing latency. Adding an instruction to search in parallel changes the evaluation prompt.
For exhaustive list questions, a few correct items is only partial success. The agent needs to check every inclusion condition and search for missing candidates. Use the benchmark's official grading rules for missing items, duplicates, and extra answers.
### Check a small run before scaling up
Inspect a fixed sample of traces before running the full benchmark. Did the model form useful queries? Did the answering model receive the excerpts? Did single-hop runs stay within one request? Did independent multi-hop calls run concurrently? Check API warnings and errors alongside answers.
Freeze the prompts and settings, then run on held-out questions. Save the exact requests, returned evidence, final answers, grades, token usage, and timings. Use a fresh `session_id` for each question and configuration, reusing it for that question's related Search and Extract calls.
Report failed and timed-out questions in the primary results with a declared scoring policy. Bound retries and include their cost and time. Retrying until success hides reliability problems. Report how often agents exhaust their budgets, and measure concurrent calls by wall time rather than summing their durations.
Use the benchmark's official grader where available. Keep any additional citation or evidence audit separate from the official score. Check retrieved pages for benchmark mirrors and published answer keys, and apply the same contamination policy across providers.
## Common pitfalls
Inspect a few complete traces before interpreting a score. Small integration mistakes change what the eval measures.
| Pitfall | What to do instead |
| - | - |
| Sending identical text in `objective` and `search_queries` | Use the objective to describe the information needed, including relevant context. Use short keyword queries to find it. The fields should share entities and intent, but each has a different job. |
| Sending the raw benchmark question straight to Search | For an agent eval, let the model reformulate the question into an objective and keyword queries before calling Search. Include this step in cost and latency. If you deliberately test raw questions, report that as a separate configuration. |
| Putting the entire research task into one keyword query | Let the agent break a complex question into focused searches. Each request should address a specific information need, with related keyword queries inside that request. |
| Repeating nearly identical keyword queries | Use additional queries for useful aliases or different angles. A simple lookup may need only one. |
| Giving the agent URLs without the returned excerpts | Pass titles, URLs, and excerpts through to the model. Inspect the final tool message to catch missing text, double-encoding, or excessive truncation. |
| Treating a multi-hop task as a single lookup | Allow follow-up searches and page reads within a declared budget. Label a one-request test as single-hop, even when the dataset contains research questions. |
| Running independent calls sequentially | Dispatch calls from the same model turn concurrently when they do not depend on each other. Track each call's latency and the task's elapsed time separately. |
| Changing several settings between providers | Keep the model, research prompt, budgets, and grader fixed. Configure provider-specific tool schemas according to their docs and save them with the run. |
| Grading only the questions that completed successfully | Include failures and timeouts in the primary results using a declared scoring policy. Record retries, rate limits, and budget exhaustion so reliability problems remain visible. |
| Letting reference answers leak into the search step | Keep gold answers out of agent context and query generation. Check retrieved pages for benchmark mirrors and published answer keys. |
## Grade answers and classify failures
### How to grade
Most teams use an LLM-as-judge: a high-end model compares the agent's final answer to verified ground truth. Require reasoning alongside the score, and audit that cited URLs support the answer's claims.
| Task shape | How to grade | Example |
| - | - | - |
| Factual question | Correctness against the gold answer, plus citation support | "What is Vendor Y's current cancellation deadline?" |
| List / discovery | Recall and precision against the gold list | "Which companies in this list announced funding in the last 30 days?" |
| Structured output / enrichment | Field-level accuracy against gold records | "Fill in CEO, HQ, and last round for these 500 companies" |
| Open-ended research | Rubric-based judging (coverage, sourcing, correctness of claims) | "Summarize the regulatory landscape for X" |
Grade an explicit "could not retrieve" above a confident wrong answer, especially on date- and jurisdiction-sensitive questions. Hand-check at least 10% of judged runs, including both failures and successes, before trusting automated scores.
### Classify failures
Not every failure is a search API problem. Classify failures into four classes:
| Class | Description | Search API fault? |
| - | - | - |
| **No search call** | The agent never invoked the tool | No |
| **Synthesis failure** | The right result was in context and the model still answered wrong | No |
| **Provider error** | Timeouts, 5xx, refusals | Yes (or account limits at eval load) |
| **Retrieval miss** | Reasonable query, wrong or missing results | Yes |
Only retrieval misses and provider errors are direct failures of the search API. Many eval failures at high load reflect rate limits rather than capability.
## Measure cost and latency
Measure end-to-end cost to complete the task: search spend plus LLM tokens the agent uses reasoning over what search returned. A cheaper-per-call API that returns noisy, low-density results can be more expensive overall, since it drives more calls, more hops, and more tokens in context.
Track these metrics for each provider and configuration:
| Metric | Definition |
| - | - |
| Cost per resolved task | Total search, extract, and model token charges divided by the number of resolved tasks |
| Total tool calls per run | Number of Search and Extract invocations the agent makes while answering each question |
| Search latency | Elapsed time for one Search tool call, from dispatch until its response or error returns, reported at p50, p95, and p99 |
| End-to-end latency | Time from user question to final answer, reported at p50, p95, and p99 |
| Tokens per task | LLM tokens the agent consumes reasoning over search results per resolved task |
Measure search latency around the tool invocation, excluding the model's query reformulation and answer generation. If the tool retries internally, include those retries and backoff in the call's elapsed time and log the individual attempts. Report errors and timeouts alongside latency percentiles. For parallel calls, record each call separately. Their summed durations are not the task's elapsed time.
Result length and hop count are tempting efficiency proxies, but they are unreliable in isolation. Verbose results can help an agent exit early. Over-compressed results can force extra hops. Parallel tool calls make hop counts undercount work. Measure end-to-end whenever you can.
## Interpret results
There is rarely a single "best" search tool. Configurations sit on a trade-off surface (a Pareto frontier). Plot results on two axes:
* Accuracy vs cost: find the cheapest mode or provider at acceptable accuracy
* Accuracy vs latency: find the fastest option at acceptable accuracy
Look for configurations that sit on the frontier in the quadrant you care about most. The single highest-accuracy point is not the answer if it is dominated on cost or latency.
# Search indexed web content for coding agents
Source: https://docs.parallel.ai/search/indexed-content-for-agents
Give coding agents documentation excerpts from the Parallel web index without a live source-page fetch
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
Coding agents need API documentation and error explanations while working in environments that cannot browse source websites. You can use the Search API to retrieve excerpts from Parallel's existing web index. Search disables live source-page fetching by default when you omit `advanced_settings.fetch_policy`.
## Search indexed documentation
Send a request to `POST /v1/search` without `advanced_settings.fetch_policy`:
```json theme={"system"}
{
"objective": "Find the official Python documentation for how asyncio TaskGroup handles cancellation.",
"search_queries": [
"Python asyncio TaskGroup cancellation docs",
"TaskGroup cancellation behavior Python documentation"
],
"advanced_settings": {
"source_policy": {
"include_domains": ["docs.python.org"]
}
}
}
```
Search returns titles, URLs, and excerpts from indexed pages that match the query. The `include_domains` filter limits the results to Python's documentation site. Remove that filter when your agent needs results from more sources. See [Source Policy](/resources/source-policy) for domain and path filters.
The index may lack a page or contain an older copy. Search does not fetch a missing or stale source page in this default mode. If you supply `advanced_settings.fetch_policy` to request fresher content, Search may fetch source pages. See [Advanced Search Settings](/search/advanced-search-settings).
## Keep URL extraction separate
If you give the agent the Extract API, it can submit a URL that did not appear in Search results. Extract uses a separate, dynamic fetch policy by default and may request the source website. Its `max_age_seconds`, `timeout_seconds`, and `disable_cache_fallback` fields control freshness, fetch duration, and fallback behavior. They do not provide a documented security guarantee against all upstream requests. See [Advanced Extract Settings](/extract/advanced-extract-settings#fetch-policy).
# Migrate to Parallel Search
Source: https://docs.parallel.ai/search/migrate-to-parallel
Move from Exa, Tavily, SERP APIs, or built-in model search to the Parallel Search API
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
Comparing providers? See [How to Eval](/search/evaluating-search) for how to run a fair, end-to-end benchmark before you switch.
This guide maps the request you make today to its Parallel Search equivalent. Parallel Search takes keyword `search_queries`, plus an optional natural language `objective`, and returns ranked, LLM-optimized excerpts in a single call. `search_queries` is the only required field, so most migrations are a one-field change. Picking a [mode](/search/modes) is optional; requests default to `advanced`.
| Coming from | Closest Parallel equivalent |
| - | - |
| Exa `instant` | Search with `mode: "turbo"` |
| Exa `fast` | Search with `mode: "fast"` |
| Exa `auto` | Search with `mode: "advanced"` |
| Exa `deep-lite` or `deep` | [Task API](/task-api/task-quickstart) or [Responses API](/responses-api/responses-quickstart) (synthesized output) |
| Exa `deep-reasoning` | [Task API](/task-api/task-quickstart) |
| Tavily `ultra-fast` | Search with `mode: "turbo"` |
| Tavily `fast` | Search with `mode: "fast"` |
| Tavily `basic` | Search with `mode: "basic"` |
| Tavily `advanced` | Search with `mode: "advanced"` |
| SERP API | Search with `mode: "turbo"` or `"basic"` (one call, ranked excerpts) |
| Built-in model search (e.g. OpenAI `web_search`) | Search as a function tool ([OpenAI](/integrations/openai-tool-calling), [Anthropic](/integrations/anthropic-tool-calling)) with `mode: "fast"` for small models and `mode: "advanced"` for large models |
## Start with one field
Exa, Tavily, and SERP APIs all center on a single query string. The fastest migration is to pass that string as one entry in `search_queries`. That alone is a complete, valid request, since `mode` is optional and defaults to `advanced`:
```json theme={"system"}
{ "search_queries": ["latest developments in solid state batteries"] }
```
For the lowest latency and cost, add `"mode": "turbo"`. For better results, also add an optional `objective`, a natural language description of your full intent, and split the query into 2-3 short keyword queries:
```json theme={"system"}
{
"objective": "Latest developments in solid state batteries. Focus on manufacturing progress and commercialization timelines.",
"search_queries": ["solid state battery progress", "solid state battery commercialization"],
"mode": "turbo"
}
```
See [Best Practices](/search/best-practices) for how to write objectives and queries.
## From Exa
| Exa parameter | Parallel equivalent |
| - | - |
| `query` | `search_queries` (the only required field); optionally add an `objective` describing your full intent |
| `type: "instant"` | `mode: "turbo"` |
| `type: "fast"` | `mode: "fast"` |
| `type: "auto"` | `mode: "advanced"` |
| `type: "deep-lite"` or `"deep"` | Use the [Task API](/task-api/task-quickstart) or [Responses API](/responses-api/responses-quickstart) (both return synthesized output) |
| `type: "deep-reasoning"` | Use the [Task API](/task-api/task-quickstart) |
| `numResults` | `advanced_settings.max_results` |
| `additionalQueries` | Add them as extra entries in `search_queries`; Parallel searches multiple queries in one call |
| `includeDomains` / `excludeDomains` | `advanced_settings.source_policy.include_domains` / `exclude_domains` |
| `startPublishedDate` | `advanced_settings.source_policy.after_date` (YYYY-MM-DD) |
| `userLocation` | `advanced_settings.location` (ISO 3166-1 alpha-2 country code) |
| `contents.text` (full page text) | Search returns compressed excerpts, not full page bodies. For full page contents, use the [Extract API](/extract/extract-quickstart) |
| `category: "company"` or `"people"` | Consider [Entity Search](/findall-api/entity-search) |
| `outputSchema` / `summary` | Structured, synthesized outputs are the [Task API](/task-api/task-quickstart) or [Responses API](/responses-api/responses-quickstart) |
Before and after:
```bash Exa theme={"system"}
curl https://api.exa.ai/search \
-H "x-api-key: $EXA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "latest developments in solid state batteries",
"type": "fast",
"numResults": 10,
"contents": { "text": { "maxCharacters": 1500 } }
}'
```
```bash Parallel theme={"system"}
curl https://api.parallel.ai/v1/search \
-H "x-api-key: $PARALLEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"search_queries": ["latest developments in solid state batteries"],
"mode": "fast"
}'
```
Excerpts are on by default and sized dynamically, so most requests need no excerpt configuration at all. Add `advanced_settings` knobs only when you have a specific product constraint. Restrictive settings can reduce result quality.
## From Tavily
Tavily's `search_depth` tiers map one-to-one onto Parallel modes.
| Tavily parameter | Parallel equivalent |
| - | - |
| `query` | `search_queries` (the only required field); optionally add an `objective` describing your full intent |
| `search_depth: "ultra-fast"` | `mode: "turbo"` |
| `search_depth: "fast"` | `mode: "fast"` |
| `search_depth: "basic"` | `mode: "basic"` |
| `search_depth: "advanced"` | `mode: "advanced"` |
| `max_results` | `advanced_settings.max_results` |
| `include_domains` / `exclude_domains` | `advanced_settings.source_policy.include_domains` / `exclude_domains` |
| `start_date` | `advanced_settings.source_policy.after_date` (YYYY-MM-DD). Tavily `time_range` (relative, e.g. `week`) has no direct equivalent; convert to an absolute date first |
| `country` | `advanced_settings.location` (ISO 3166-1 alpha-2; convert Tavily's country name to a code) |
| `topic: "news"` or `"finance"` | State the focus in your `objective` (for example "recent news about..." or "latest financial results for...") |
| `include_answer` | Search returns excerpts for your model to reason over. For grounded completions, use the [Responses API](/responses-api/responses-quickstart) |
| `include_raw_content` | Full page contents are the [Extract API](/extract/extract-quickstart) |
Before and after:
```bash Tavily theme={"system"}
curl https://api.tavily.com/search \
-H "Authorization: Bearer $TAVILY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "latest developments in solid state batteries",
"search_depth": "fast",
"max_results": 10
}'
```
```bash Parallel theme={"system"}
curl https://api.parallel.ai/v1/search \
-H "x-api-key: $PARALLEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"search_queries": ["latest developments in solid state batteries"],
"mode": "fast"
}'
```
Parallel authenticates with an `x-api-key` header rather than `Authorization: Bearer`, so a request that keeps the Tavily-style Bearer header returns 401.
## From a SERP API
A SERP API returns a ranked list of links and snippets, but you must make subsequent fetches to extract usable information.
Parallel Search combines everything into a single call. Instead of links you fetch and process, you get ranked, compressed excerpts sized for a model, so there is no fetch round and no chunking or ranking to build yourself. If a workflow still needs a specific page's full contents, use the [Extract API](/extract/extract-quickstart).
You search, then fetch and process each result in your own code:
```python theme={"system"}
import os, requests
from bs4 import BeautifulSoup
# split_into_chunks() and rank_by_relevance() are helpers you write and maintain
query = "solid state battery developments"
# Step 1: search returns links + snippets, no page content
hits = requests.get("https://serpapi.com/search", params={
"q": query, "api_key": os.environ["SERPAPI_KEY"],
}).json()["organic_results"][:10]
# Step 2: fetch every result URL yourself, one at a time
pages = []
for h in hits:
try:
html = requests.get(h["link"], timeout=10).text
except requests.RequestException:
continue # dead links, timeouts, bot blocks: your problem to handle
text = BeautifulSoup(html, "html.parser").get_text(" ", strip=True)
# (a scraper that returns markdown lets you skip this HTML parsing)
pages.append(text)
# Step 3: chunk and rank locally so it fits the model context
chunks = [c for p in pages for c in split_into_chunks(p)]
context = rank_by_relevance(chunks, query)[:15]
```
Parallel does it in one call:
```python theme={"system"}
import os
from parallel import Parallel
client = Parallel(api_key=os.environ["PARALLEL_API_KEY"])
# one call returns ranked, model-ready excerpts
search = client.search(search_queries=["solid state battery developments"], mode="turbo")
context = [ex for r in search.results for ex in r.excerpts]
```
You search, then fetch each result:
```bash theme={"system"}
# Step 1: search returns links + snippets, no page content
curl "https://serpapi.com/search?q=solid+state+battery+developments&api_key=$SERPAPI_KEY"
# Step 2: fetch each result URL for its content (repeat for every link),
# then strip HTML, chunk, and rank in your own code
curl -L "https://"
curl -L "https://"
```
Parallel does it in one call:
```bash theme={"system"}
curl https://api.parallel.ai/v1/search \
-H "x-api-key: $PARALLEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "search_queries": ["solid state battery developments"], "mode": "turbo" }'
```
## From built-in model search
"Built-in model search" splits into two cases that migrate differently.
**Building on the API.** If you call a provider's hosted search tool in your own app (for example OpenAI's `web_search` in the Responses API), the provider runs the search inside its own walls. Migrate by registering Parallel Search as a function tool your app executes, which gives you mode control, the excerpts, and the provider choice. Match the mode to the model driving the tool: use `fast` (\~700ms) for small models, and `advanced` for large models that can spend more latency for higher-quality results.
```python theme={"system"}
def search_web(search_queries: list[str], objective: str | None = None) -> dict:
return parallel_client.search(
search_queries=search_queries,
objective=objective,
# Small model driving the tool → "fast". Large model → "advanced".
mode="fast",
)
```
The model decides when to search. Your handler decides how. Full walkthroughs with the tool schema and the tool-result loop live on the [OpenAI](/integrations/openai-tool-calling), [Anthropic](/integrations/anthropic-tool-calling), and [Ollama](/integrations/ollama-tool-calling) pages.
**Inside the ChatGPT app (no code).** If you rely on web search in ChatGPT itself rather than building on the API, add Parallel as a connector through the [Search MCP](/integrations/mcp/search-mcp).
## What you parse now
Parallel returns a `results` array. Each item has a `url`, `title`, `publish_date`, and `excerpts` (the compressed, relevant snippets your model reads), with a top-level `warnings` field worth checking on early calls:
```json theme={"system"}
{
"results": [
{ "url": "https://...", "title": "...", "publish_date": null, "excerpts": ["..."] }
],
"warnings": null
}
```
If your current integration expects a field Search does not return, here is where that capability lives:
| If your integration expects | With Parallel Search | Next step |
| - | - | - |
| a relevance `score` to sort on | results come back already ranked | preserve the returned order; there is no per-result score |
| full page text (Exa `text`, Tavily `raw_content`) | you get compressed excerpts, not full page bodies | use the [Extract API](/extract/extract-quickstart) for full contents |
| a synthesized answer or report (Tavily `include_answer`, Exa `summary` / `deep`) | Search returns excerpts for your model to reason over | use the [Responses API](/responses-api/responses-quickstart) for grounded answers, or the [Task API](/task-api/task-quickstart) for multi-step research |
| a list of people or companies (Exa `category`) | Search retrieves pages and excerpts across the web | use [Entity Search](/findall-api/entity-search) |
| `images` | not returned | — |
## Verify your migration
Before moving production traffic, run your top queries in the [Playground](https://platform.parallel.ai/play/search) and start with the mapped mode. On your first calls, check the response `warnings` field for any parameters that were adjusted or ignored, and confirm [Pricing](/getting-started/pricing) and [Rate Limits](/getting-started/rate-limits) for your volume.
# Search Modes
Source: https://docs.parallel.ai/search/modes
Configure the Search API mode for your use case
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
Search offers four modes, in order of latency. Use `turbo` for the lowest latency and cost in real-time, high-volume workloads; `fast` for high-quality, sub-second search across most agent workloads; `basic` for quick retrieval with extended snippets per result; and `advanced` for the highest-quality, multi-hop results. If `mode` isn't set, requests default to `advanced`.
Not sure which mode to pick? Start with `fast`. It delivers high-quality results in around 700ms and is the right choice for most agents. Reach for `turbo` when latency and cost matter most for simple, high-volume lookups, `basic` when you want extended snippets and can spend around a second per call, and `advanced` when result quality matters more than latency.
## Available modes
| Mode | What it does | Latency | Cost | Best for |
| - | - | - | - | - |
| `turbo` | Lowest latency and cost; built to ground every call. | \~200ms | \$1 per 1,000 requests | Latency-sensitive, high-volume workloads (e.g. voice, chat, web search tools, RAG pre-filtering, high-volume lookups) |
| `fast` | High-quality search with sub-second latency. Recommended default for most agents. | \~700ms | \$1 per 1,000 requests | Most agents: interactive assistants, tool-calling loops, and any workload that needs quality results without multi-second latency |
| `basic` | Returns extended snippets per result for richer per-call context. Works best with 2-3 high-quality `search_queries`. | \~1s | \$5 per 1,000 requests | Agents that benefit from longer excerpts per source in a single call |
| `advanced` (default) | Uses a more advanced retrieval and compression pipeline for higher-quality results. | \~3s | \$5 per 1,000 requests | Multi-hop background agents that can tolerate extra latency for better depth and cost-efficiency (e.g., code review agents, deep research) |
Turbo currently supports English and Japanese-language queries. For broader multilingual coverage, use `basic` or `advanced`.
## Example
Switching modes is a single parameter change. The rest of your request stays the same:
```json theme={"system"}
{
"mode": "fast",
"objective": "What is the current price of NVIDIA stock?",
"search_queries": ["NVIDIA stock price", "NVDA quote today"]
}
```
# Search MCP
Source: https://docs.parallel.ai/search/search-mcp
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
# Upgrade from Beta to GA
Source: https://docs.parallel.ai/search/search-migration-guide
Migrate from Beta to GA (V1) Search API
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
This guide helps you migrate from the Beta Search API (`/v1beta`) to the GA version (`/v1`).
The V1 Search API is generally available and recommended for all new integrations. This guide explains how to migrate existing Beta integrations to V1's current modes and request fields.
## Highlights
1. **`search_queries` is now required** — At least one non-empty query must be provided. In Beta, only one of `objective` or `search_queries` was required.
2. **Settings reorganized under `advanced_settings`** — `source_policy`, `fetch_policy`, excerpt settings, and `max_results` are now nested under a single new `advanced_settings` object (previously top-level fields). See [Advanced Settings](/search/advanced-search-settings) for more details.
3. **New `location` field** — Set `advanced_settings.location` to an ISO 3166-1 alpha-2 country code (e.g., `"us"`, `"gb"`, `"de"`, `"jp"`) to geo-target search results. Only a subset of countries are currently supported; unsupported or invalid values are ignored with a warning.
4. **Updated modes** — V1 has four modes (`turbo`, `fast`, `basic`, `advanced`), with `advanced` as the new default. For most use cases, map both `fast` and `one-shot` to `fast`, and map `agentic` to `advanced`. V1 `fast` delivers high-quality results in around 700ms and is the recommended default. V1 `basic` remains available if you want longer excerpts similar to the old `one-shot` output.
* **`fast`**: The suggested replacement for Beta `fast` and `one-shot`, and the recommended default for most agents. It delivers high-quality results in around 700ms.
* **`basic`**: Returns extended snippets per result and works best with 2-3 high-quality search\_queries. Choose it when you want longer excerpts per source in a single call, similar to the old `one-shot`-style output.
* **`advanced`** (default): The V1 equivalent of Beta `agentic`. Provides higher quality with more advanced retrieval and compression. Best for complex queries where result quality matters more than latency.
* **`turbo`** (new in V1): A lower-latency, lower-cost mode for real-time, high-volume grounding, with no Beta equivalent. It typically returns results in around 200ms. See [Search Modes](/search/modes).
## Overview of Changes
| Component | Beta | V1 |
| - | - | - |
| **Endpoint** | `/v1beta/search` | `/v1/search` |
| **Modes** | `fast`, `one-shot`, `agentic` (default `one-shot`) | `turbo`, `fast`, `basic`, `advanced` (default `advanced`). Beta `fast` and `one-shot` map to `fast`; `agentic` maps to `advanced`. Choose `basic` for longer excerpts similar to the old `one-shot`-style output. |
| **SDK method** | `client.beta.search()` (`parallel-web` before 1.0) | `client.search()` (`parallel-web` 1.0+) |
| **`search_queries`** | Optional (one of `objective` or `search_queries` required) | Required (at least one non-empty query) |
| **`objective`** | Required if `search_queries` omitted | Optional |
| **`max_chars_total`** | Inside `excerpts` object | Promoted to top-level request field |
| **`client_model`** (new) | — | Top-level field for model-specific optimizations |
| **`location`** (new) | — | `advanced_settings.location` — ISO 3166-1 alpha-2 country code for geo-targeted results |
| **`advanced_settings`** (new) | — | New object nesting `source_policy`, `fetch_policy`, `excerpt_settings`, `max_results`, and `location` |
If your integration uses `client.beta.search()`, upgrade to the latest `parallel-web` release and replace it with `client.search()`.
## Migration Example
### Before (Beta)
```bash cURL theme={"system"}
curl https://api.parallel.ai/v1beta/search \
-H "Content-Type: application/json" \
-H "x-api-key: $PARALLEL_API_KEY" \
-d '{
"objective": "Find latest information about Parallel Web Systems. Focus on new product releases, benchmarks, or company announcements.",
"search_queries": ["Parallel Web Systems products", "Parallel Web Systems announcements"],
"mode": "fast",
"excerpts": {
"max_chars_per_result": 10000,
"max_chars_total": 50000
}
}'
```
### After (V1)
```bash cURL theme={"system"}
curl https://api.parallel.ai/v1/search \
-H "Content-Type: application/json" \
-H "x-api-key: $PARALLEL_API_KEY" \
-d '{
"objective": "Find latest information about Parallel Web Systems. Focus on new product releases, benchmarks, or company announcements.",
"search_queries": ["Parallel Web Systems products", "Parallel Web Systems announcements"],
"mode": "fast",
"max_chars_total": 50000,
"advanced_settings": {
"excerpt_settings": {
"max_chars_per_result": 10000
}
}
}'
```
```python Python theme={"system"}
from parallel import Parallel
import os
client = Parallel(api_key=os.environ["PARALLEL_API_KEY"])
search = client.search(
objective="Find latest information about Parallel Web Systems. Focus on new product releases, benchmarks, or company announcements.",
search_queries=["Parallel Web Systems products", "Parallel Web Systems announcements"],
mode="fast",
max_chars_total=50000,
advanced_settings={"excerpt_settings": {"max_chars_per_result": 10000}},
)
print(search.results)
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({ apiKey: process.env.PARALLEL_API_KEY });
const search = await client.search({
objective: "Find latest information about Parallel Web Systems. Focus on new product releases, benchmarks, or company announcements.",
search_queries: ["Parallel Web Systems products", "Parallel Web Systems announcements"],
mode: "fast",
max_chars_total: 50000,
advanced_settings: { excerpt_settings: { max_chars_per_result: 10000 } },
});
console.log(search.results);
```
## Additional Resources
* [Search Quickstart](/search/search-quickstart) - Get started with the Search API
* [Best Practices](/search/best-practices) - Optimize your search requests
* [Search MCP](/integrations/mcp/search-mcp) - Use Search via Model Context Protocol
* [API Reference](/api-reference/search/search) - Complete parameter specifications
Questions? Contact [support@parallel.ai](mailto:support@parallel.ai).
# Search API Quickstart
Source: https://docs.parallel.ai/search/search-quickstart
Execute natural language web searches and retrieve LLM-optimized excerpts with the Parallel Search API
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
The **Search API** takes a natural language objective and returns relevant excerpts
optimized for LLMs, replacing multiple keyword searches with a single call for broad or
complex queries.
**Available via MCP**: Search is available as a tool as part of the Parallel Search MCP. Our MCP is optimized for best practices on Search and Extract usage. [Start here](/integrations/mcp/search-mcp) with MCP for your use case. If you're interested in direct use of the API, follow the steps below.
**Looking for people or companies?** [Entity Search](/findall-api/entity-search) is a fast, synchronous people-and-company search — the real-time counterpart to FindAll — returning a set of matching results in seconds. It is distinct from the Search API, which retrieves pages and excerpts across the whole web.
## 1. Set Up Prerequisites
Generate your API key on [Platform](https://platform.parallel.ai). Then, set up with the TypeScript SDK, Python SDK or with cURL:
```bash cURL theme={"system"}
echo "Install curl and jq via brew, apt, or your favorite package manager"
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
```bash Python theme={"system"}
pip install parallel-web
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
```bash TypeScript theme={"system"}
npm install parallel-web
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
## 2. Execute Your First Search
### Sample Request
```bash cURL theme={"system"}
curl https://api.parallel.ai/v1/search \
-H "Content-Type: application/json" \
-H "x-api-key: $PARALLEL_API_KEY" \
-d '{
"objective": "Find latest information about Parallel Web Systems. Focus on new product releases, benchmarks, or company announcements.",
"search_queries": [
"Parallel Web Systems products",
"Parallel Web Systems announcements"
]
}'
```
```python Python theme={"system"}
import os
from parallel import Parallel
client = Parallel(api_key=os.environ["PARALLEL_API_KEY"])
search = client.search(
objective="Find latest information about Parallel Web Systems. Focus on new product releases, benchmarks, or company announcements.",
search_queries=[
"Parallel Web Systems products",
"Parallel Web Systems announcements"
],
)
for result in search.results:
print(f"{result.title}: {result.url}")
for excerpt in result.excerpts:
print(excerpt[:200])
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({ apiKey: process.env.PARALLEL_API_KEY });
async function main() {
const search = await client.search({
objective: "Find latest information about Parallel Web Systems. Focus on new product releases, benchmarks, or company announcements.",
search_queries: [
"Parallel Web Systems products",
"Parallel Web Systems announcements"
],
});
for (const result of search.results) {
console.log(`${result.title}: ${result.url}`);
for (const excerpt of result.excerpts) {
console.log(excerpt.slice(0, 200));
}
}
}
main().catch(console.error);
```
This request uses the default `advanced` mode (\~3s, highest quality). For most agents, add `"mode": "fast"` for high-quality results in around 700ms; use `"mode": "turbo"` (p50 of 200ms) when latency and cost matter most. The rest of the request stays the same. See [Search Modes](/search/modes) for a runnable example.
### Sample Response
The API returns a JSON response with the following structure. The SDK examples above iterate over `results` to print each result's title, URL, and excerpts. If any non-fatal input adjustments or validation warnings have occurred, they will be in the warnings field.
```json [expandable] theme={"system"}
{
"search_id": "search_8a911eb27c7a4afaa20d0d9dc98d07c0",
"results": [
{
"url": "https://www.linkedin.com/posts/analytics-india-magazine_former-twitter-ceo-parag-agrawals-new-startup-activity-7394607179896426497-xdcJ",
"title": "Parallel Web Systems raises $100M for AI web access - LinkedIn",
"publish_date": null,
"excerpts": [
"Parallel Web Systems raises $100M for AI web access - LinkedIn\nHis new startup, Parallel Web Systems, has just secured a massive $100 million Series A round at a striking $740 million post-money valuation."
]
},
{
"url": "https://www.reuters.com/business/ex-twitter-ceo-agrawals-ai-search-startup-parallel-raises-100-million-2025-11-12",
"title": "Ex-Twitter CEO Agrawal's AI search startup Parallel raises ... - Reuters",
"publish_date": null,
"excerpts": [
"Section Title: Information you can trust > Follow Us > LSEG Products\nContent:\nWorkspace, opens new tab — Access unmatched financial data, news and content in a highly-customised workflow experience on desktop, web and mobile.\n\n... (content truncated)"
]
},
{
"url": "https://www.linkedin.com/posts/tbpn_parag-agrawal-and-the-team-at-parallel-web-activity-7395150680069627905-oMIZ",
"title": "Parallel Web Systems raises $100M for AI data search. - LinkedIn",
"publish_date": null,
"excerpts": [
"Nov 14, 2025 · Today we're excited to announce new funding for LangChain (at a $1.25B valuation) to allow us to build the platform for agent engineering."
]
},
{
"url": "https://parallel.ai/products/search",
"title": "The best web search for your AI | Parallel | Parallel Web Systems | Web Search & Research APIs Built for AI Agents",
"publish_date": null,
"excerpts": [
"Section Title: The best web search for your AI > We optimize every web token in the context window\nContent:\nThis means agent responses are more accurate and lower cost.\n\nBrowseComp benchmark proving Parallel's enterprise deep research API delivers 48% accuracy vs GPT-4's 1% browsing capability.\n\n... (content truncated)"
]
},
{
"url": "https://startups.gallery/companies/parallel",
"title": "Parallel | startups.gallery",
"publish_date": null,
"excerpts": [
"Section Title: Parallel\nContent:\nAt Parallel Web Systems, we are bringing a new web to life: it's built with, by, and for AIs. Our work spans innovations across crawling, indexing, ranking, retrieval, and reasoning systems. Our first product is a set of APIs for AIs to do more with web data. Founded by former CEO/CTO of Twitter, Parag Agrawal.\n\nBacked by First Round Capital, Index Ventures, Khosla Ventures."
]
},
{
"url": "https://www.instagram.com/popular/find-latest-information-about-parallel-web-systems-focus-on-new-product-releases-benchmarks-or-company-announcements",
"title": "Find Latest Information About Parallel Web Systems Focus On New ...",
"publish_date": null,
"excerpts": [
"Find Latest Information About Parallel Web Systems Focus On New ...\nWatch short videos about find latest information about parallel web systems focus on new product releases benchmarks or company announcements from people"
]
},
{
"url": "https://parallel.ai/",
"title": "Parallel Web Systems | Infrastructure for intelligence on the web",
"publish_date": null,
"excerpts": [
"Section Title: Highest accuracy at every price point\nContent:\nState of the art across the most challenging benchmarks.\n\nSection Title: Towards a programmatic web for AIs\nContent:\nParallel is building new interfaces, infrastructure, and business models for AIs to work with the web.\n\n... (content truncated)"
]
},
{
"url": "https://www.linkedin.com/company/parallel-web",
"title": "Parallel Web Systems",
"publish_date": null,
"excerpts": [
"Section Title: Parallel Web Systems > About us\nContent:\nAt Parallel Web Systems, we are bringing a new web to life: it's built with, by, and for AIs. Our work spans innovations across crawling, indexing, ranking, retrieval, and reasoning systems. Our first product is a set of APIs for AIs to do more with web data.\n\nHeadquarters: Palo Alto, California. Company size: 11-50 employees.\n\n... (content truncated)"
]
},
{
"url": "https://www.prnewswire.com/news-releases/genpact-and-parallel-web-systems-partner-to-drive-tangible-efficiency-from-ai-systems-302736563.html",
"title": "Genpact and Parallel Web Systems Partner to Drive Tangible ...",
"publish_date": null,
"excerpts": [
"NEW YORK and PALO ALTO, Calif., April 8, 2026 /PRNewswire/ -- Genpact and Parallel Web Systems today announced a partnership leveraging Parallel's AI-native web agents and web search tools to transform how Genpact addresses enterprise challenges in information search and retrieval across business operations.\n\n... (content truncated)"
]
},
{
"url": "https://aimagazine.com/magazines/parag-agrawals-parallel-web-systems-raises-100m-for-ai",
"title": "How Parag Agrawal's Parallel Web Systems Raised $100m for AI | AI Magazine",
"publish_date": "2025-11-19",
"excerpts": [
"Parallel Web Systems, a startup from former Twitter CEO Parag Agrawal, has secured US$100m in a Series A funding round. The investment round was co-led by Kleiner Perkins and Index Ventures with participation from other backers, including Khosla Ventures.\n\nFounded in 2023 and officially launched in August 2025, Parallel Web Systems develops APIs that enable AI systems to search the live web for current information needed to perform tasks.\n\n... (content truncated)"
]
}
],
"warnings": null,
"usage": [
{
"name": "sku_search",
"count": 1
}
],
"session_id": "session_8a911eb27c7a4afaa20d0d9dc98d07c0"
}
```
## Next Steps
* **[Deploy on Render](/integrations/render)**: run a research agent with Search, Extract, and Render Workflows
* **[Best Practices](/search/best-practices)** — learn how to craft effective objectives and search queries
* **[Search Modes](/search/modes)** — explore mode presets for different use cases
* **[API Reference](/api-reference/search/search)** — full parameter specifications, constraints, and response schema
* **[Rate Limits](/getting-started/rate-limits)** — default quotas per product
# Source Policy
Source: https://docs.parallel.ai/search/source-policy
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
# Create App
Source: https://docs.parallel.ai/service-api/apps/create-app
https://api.parallel.ai/account/service/openapi.json post /service/v1/apps
Create a new app for the authenticated organization
# Delete App
Source: https://docs.parallel.ai/service-api/apps/delete-app
https://api.parallel.ai/account/service/openapi.json delete /service/v1/apps/{app_id}
Delete an app from the authenticated organization
# List Apps
Source: https://docs.parallel.ai/service-api/apps/list-apps
https://api.parallel.ai/account/service/openapi.json get /service/v1/apps
List all apps for the authenticated organization
# Add to Balance
Source: https://docs.parallel.ai/service-api/balance/add-to-balance
https://api.parallel.ai/account/service/openapi.json post /service/v1/balance/add
Charge the organization's default payment method and add the amount to the prepaid credit balance. The default payment method configured on the org's Stripe customer is always used; no payment method id is accepted from the client.
# Get Balance
Source: https://docs.parallel.ai/service-api/balance/get-balance
https://api.parallel.ai/account/service/openapi.json get /service/v1/balance
Get the authenticated organization's prepaid credit balance
# Create Key
Source: https://docs.parallel.ai/service-api/keys/create-key
https://api.parallel.ai/account/service/openapi.json post /service/v1/apps/{app_id}/keys
Create a new API key for an app
# Delete Key
Source: https://docs.parallel.ai/service-api/keys/delete-key
https://api.parallel.ai/account/service/openapi.json delete /service/v1/apps/{app_id}/keys/{api_key_id}
Delete an API key from an app
# Task API Best Practices
Source: https://docs.parallel.ai/task-api/best-practices
Best practices for writing Task Specs and caching outputs when using Parallel's agent products
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## Task Spec Best Practices
Define what entity you're researching (input) and what specific data points you need back (output). Keep both as flat-structured as possible.
**The `description` field is your primary tool for controlling output quality.** Field-level descriptions are the best way to include specific instructions for each output field. Think of the `description` as your "prompt" for that field—it's where you specify format requirements, data sources, fallback behavior, and any other instructions.
* If executing a Deep Research style Task, use the Task Spec with `auto` schema
* If control and specificity with regards to outputs are required, use Task Spec with a JSONSchema for inputs and outputs
* In other cases, the Task Spec may not be necessary; the system in this case will output a plain text response
* When using only text based inputs, be as specific as possible about what you are expecting the system to return. Include any instructions and preferences in the input text.
* When using JSON Schema inputs, use the minimum fields required to uniquely identify the entity you want to enrich. For example, include both the company\_name and company\_website, or both the person\_name and social\_url, to help the system disambiguate.
* Avoid deeply nested structures and keep the input schema flat
**Use field-level `description` for all instructions.** The `description` field is the most effective way to control how each output field is populated. Include:
* **Entity**: What are you researching?
* **Action**: What do you want to find?
* **Specifics**: Constraints, time periods, formatting requirements
* **Error Handling**: What to return if data is unavailable (e.g., "If unavailable, return null")
**Example of a well-written description:**
```json theme={"system"}
"employee_count": {
"type": "string",
"description": "The current number of employees at the company. Use the most recent data available from LinkedIn, company website, or press releases. Format as a range (e.g., '501-1000') if exact count unavailable. If no data found, return 'Unknown'."
}
```
* Use clear, descriptive field names
* Use `ceo_name` instead of `name`
* Use `headquarters_address`\*\* instead of `address`
* Use `annual_revenue_2024`\*\* instead of `revenue`
* Specify Data Formats
* Always specify format for dates: `YYYY-MM-DD`
* Use ranges for numerical values with units: `revenue_in_millions`, `employee_count`
* Specify quantities for lists: `top_5_products`, `recent_3_acquisitions`
* **Unnecessary Fields**: Don't include fields like `reasoning` or `confidence_score` as these are already included in the basis
If there are additional requirements or instructions separate from individual fields, the top-level `description` field is available. For example:
```json theme={"system"}
{
"type": "object",
"description": "Extract all information only from well-known government sites.",
"properties": {
"latest_funding_amount": {
"type": "string",
"description": "Funding amount in millions USD format (e.g., '50M'). If unavailable, return null."
},
"funding_round_type": {
"type": "string",
"description": "Type of funding round (Series A, Series B, etc.). If unknown, return 'Type unknown'."
},
"funding_date": {
"type": "string",
"description": "Date of funding round in YYYY-MM-DD format. If partial date available, use YYYY-MM or YYYY."
},
"current_employee_count": {
"type": "string",
"description": "Current number of employees as approximate number or range. Allow estimates when precise counts unavailable."
}
}
}
```
## Caching Best Practices for Agent Products
When using Parallel's agent products (such as the Task API, [FindAll](/findall-api/findall-quickstart), or [Monitor](/monitor-api/monitor-quickstart)), inputs provide context to the model's response. Because outputs may reflect the inputs provided, developers should use caution when deciding whether to cache and reuse outputs across different end users or contexts, as that could potentially cause inputs or context from inputs to be shared across end users.
* If your inputs could include private customer data, internal documents, or any proprietary context, we do not recommend caching outputs and reusing them across customers. Because output is contextually tied to that input, private inputs could then be shared across end users.
### Matrix: How to think about caching
| API | Description | Can inputs passed in prompts be outputs? | Caching Suitability when sensitive input provided |
| - | - | - | - |
| Search, Extract, Entity Search | Fetches raw, public web content - no user context present in output | No | Yes |
| Task API, [FindAll](/findall-api/findall-quickstart), [Monitor](/monitor-api/monitor-quickstart) | Takes prompts/inputs that you provide, and those inputs can shape the output and be present in it. | Yes | No |
# Data Connectors
Source: https://docs.parallel.ai/task-api/data-connectors
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
# Interactive Research
Source: https://docs.parallel.ai/task-api/examples/interactive-research
Build an interactive research app with multi-turn context chaining
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## Overview
This guide walks through building an interactive research application using [interactions](/task-api/guides/interactions). By chaining `interaction_id` values across requests, each follow-up question automatically has the full context of prior turns — so you can drill deeper into a topic without restating what was already researched.
## Example: Multi-Turn Research Session
Consider a research session where each question builds on the last:
1. **"Which country won the most Winter Olympics gold medals in 2026?"** — initial broad question
2. **"How many medals did they win?"** — a follow-up that only makes sense with context from step 1
3. **"How does that compare to the second place country?"** — drills deeper into the same thread
Without interactions, each request would be independent and the API wouldn't know what "they" or "second place" refers to. With interactions, context carries forward automatically.
```python Python theme={"system"}
import os
from parallel import Parallel
from parallel.types import TaskSpecParam, TextSchemaParam
client = Parallel(api_key=os.environ["PARALLEL_API_KEY"])
# Turn 1: Initial question
run1 = client.task_run.create(
input="Which country won the most Winter Olympics gold medals in 2026?",
processor="lite",
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
)
result1 = client.task_run.result(run1.run_id, api_timeout=3600)
print(f"Turn 1: {result1.output.content}")
# Turn 2: Follow-up — "they" refers to the country from Turn 1
run2 = client.task_run.create(
input="How many medals did they win?",
processor="lite",
previous_interaction_id=run1.interaction_id,
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
)
result2 = client.task_run.result(run2.run_id, api_timeout=3600)
print(f"Turn 2: {result2.output.content}")
# Turn 3: Drill deeper — context from both prior turns is available
run3 = client.task_run.create(
input="How does that compare to the second place country?",
processor="lite",
previous_interaction_id=run2.interaction_id,
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
)
result3 = client.task_run.result(run3.run_id, api_timeout=3600)
print(f"Turn 3: {result3.output.content}")
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
async function pollResult(runId: string) {
for (let i = 0; i < 20; i++) {
try {
return await client.taskRun.result(runId, { timeout: 25 });
} catch (error) {
if (i === 19) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
// Turn 1: Initial question
const run1 = await client.taskRun.create({
input: "Which country won the most Winter Olympics gold medals in 2026?",
processor: "lite",
task_spec: { output_schema: { type: "text" } },
});
const result1 = await pollResult(run1.run_id);
console.log(`Turn 1: ${result1.output.content}`);
// Turn 2: Follow-up — "they" refers to the country from Turn 1
const run2 = await client.taskRun.create({
input: "How many medals did they win?",
processor: "lite",
previous_interaction_id: run1.interaction_id,
task_spec: { output_schema: { type: "text" } },
});
const result2 = await pollResult(run2.run_id);
console.log(`Turn 2: ${result2.output.content}`);
// Turn 3: Drill deeper — context from both prior turns is available
const run3 = await client.taskRun.create({
input: "How does that compare to the second place country?",
processor: "lite",
previous_interaction_id: run2.interaction_id,
task_spec: { output_schema: { type: "text" } },
});
const result3 = await pollResult(run3.run_id);
console.log(`Turn 3: ${result3.output.content}`);
```
```bash cURL theme={"system"}
# Turn 1: Initial question
echo "Turn 1: Which country won the most Winter Olympics gold medals in 2026?"
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"input": "Which country won the most Winter Olympics gold medals in 2026?",
"processor": "lite",
"task_spec": { "output_schema": { "type": "text" } }
}')
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
INTERACTION_ID=$(echo "$RUN_JSON" | jq -r '.interaction_id')
RESULT=$(curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}")
echo "$RESULT" | jq -r .output.content
# Turn 2: Follow-up — "they" refers to the country from Turn 1
echo "Turn 2: How many medals did they win?"
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"input\": \"How many medals did they win?\",
\"processor\": \"lite\",
\"previous_interaction_id\": \"${INTERACTION_ID}\",
\"task_spec\": { \"output_schema\": { \"type\": \"text\" } }
}")
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
INTERACTION_ID=$(echo "$RUN_JSON" | jq -r '.interaction_id')
RESULT=$(curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}")
echo "$RESULT" | jq -r .output.content
# Turn 3: Drill deeper — context from both prior turns is available
echo "Turn 3: How does that compare to the second place country?"
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"input\": \"How does that compare to the second place country?\",
\"processor\": \"lite\",
\"previous_interaction_id\": \"${INTERACTION_ID}\",
\"task_spec\": { \"output_schema\": { \"type\": \"text\" } }
}")
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
RESULT=$(curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}")
echo "$RESULT" | jq -r .output.content
```
## Building a Research Chat Loop
Wrap the interaction pattern in a loop to create a fully interactive research agent. Each user question chains off the previous answer's context.
```python Python theme={"system"}
import os
from parallel import Parallel
from parallel.types import TaskSpecParam, TextSchemaParam
client = Parallel(api_key=os.environ["PARALLEL_API_KEY"])
interaction_id = None
while True:
user_input = input("You: ")
if user_input.lower() in ("exit", "quit"):
break
task_run = client.task_run.create(
input=user_input,
processor="lite",
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
**({"previous_interaction_id": interaction_id} if interaction_id else {}),
)
result = client.task_run.result(task_run.run_id, api_timeout=3600)
interaction_id = task_run.interaction_id
print(f"\nAgent: {result.output.content}\n")
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
import * as readline from "readline";
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function ask(prompt: string): Promise {
return new Promise((resolve) => rl.question(prompt, resolve));
}
let interactionId: string | undefined;
while (true) {
const userInput = await ask("You: ");
if (["exit", "quit"].includes(userInput.toLowerCase())) break;
const taskRun = await client.taskRun.create({
input: userInput,
processor: "lite",
task_spec: { output_schema: { type: "text" } },
...(interactionId ? { previous_interaction_id: interactionId } : {}),
});
let result;
for (let i = 0; i < 20; i++) {
try {
result = await client.taskRun.result(taskRun.run_id, { timeout: 25 });
break;
} catch (error) {
if (i === 19) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
interactionId = taskRun.interaction_id;
console.log(`\nAgent: ${result.output.content}\n`);
}
rl.close();
```
## Next Steps
* [**Deep Research:**](/task-api/examples/task-deep-research) Comprehensive single-turn research — includes an example of chatting with deep research results
* [**Interactions:**](/task-api/guides/interactions) Full reference for the `interaction_id` and `previous_interaction_id` fields
* [**Enrichment:**](/task-api/examples/task-enrichment) Structured data enrichment
* [**Streaming Events:**](/task-api/task-sse) Monitor long-running research tasks in real time
# Task API Deep Research Quickstart
Source: https://docs.parallel.ai/task-api/examples/task-deep-research
Transform natural language queries into comprehensive intelligence reports
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## Overview
Deep Research is designed for open-ended research questions where you don't have structured input data to enrich. Instead of bringing data to enhance, you bring a research question or topic, and the Task API conducts comprehensive multi-step web exploration to deliver analyst-grade intelligence.
This powerful capability compresses hours of manual research into minutes, delivering high-quality intelligence at scale. Optimized within the `pro` and `ultra` [processor families](/task-api/guides/choose-a-processor), Deep Research transforms natural language research queries into comprehensive reports complete with inline citations and verification.
For latency-sensitive workflows—interactive applications or agentic research where a caller is waiting on results—use the [Responses API](/responses-api/responses-quickstart) instead. See [Latency-Sensitive Workflows](/task-api/guides/choose-a-processor#latency-sensitive-workflows) for details.
This guide focuses on **Deep Research**. If you have structured data you want to enrich with web intelligence (like adding columns to a spreadsheet), see our [Enrichment guide](/task-api/examples/task-enrichment).
## How Deep Research Works
With Deep Research, the system automatically:
1. Interprets your research intent from natural language
2. Conducts multi-step web exploration across authoritative sources
3. Synthesizes findings into structured data or markdown reports
4. Provides citations and confidence levels for verification
## Key Features
* **Natural Language Input**: Simply describe what you want to research in plain language—no need for structured data or predefined schemas.
* **Declarative Approach**: Specify what intelligence you need, and the system handles the complex orchestration of research, exploration, and synthesis.
* **Flexible Output Structure**: Choose between `auto` schema mode (automatically structured JSON), `text` mode (markdown reports) or pre-specified structured JSON schema based on your needs.
* **Comprehensive Intelligence**: Multi-step research across authoritative sources with granular citations, reasoning, and confidence levels for every finding.
Long-Running Tasks: Deep Research can take up to 45 minutes to complete. See [Polling vs Webhooks vs SSE](#polling-vs-webhooks-vs-sse) below for how to handle async results.
## Creating a Deep Research Task
Deep Research accepts any input schema as input, including plain-text strings.
The more specific and detailed your input, the better the research results would be.
**Input size restriction**: Deep Research is optimized for concise research
prompts and is not meant for long context inputs. Keep your input under
**15,000 characters** for optimal performance and results.
Deep Research supports two output formats to meet different integration needs:
### Auto Schema
Specifying auto schema mode in the Task API output schema triggers Deep Research
and ensures well-structured outputs, without the need to specify a desired output structure.
The final schema type will follow a [JSONSchema](/api-reference/tasks/create-task-run#body-task-spec-output-schema)
format and will be determined by the processor automatically.
Auto schema mode is the default mode when using `pro` and `ultra` line of processors.
This format is ideal for programmatic processing, data analysis, and integration with other systems.
```python Python theme={"system"}
from parallel import Parallel
client = Parallel(api_key="PARALLEL_API_KEY")
task_run = client.task_run.create(
input="Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity and other relevant details.",
processor="ultra"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env["PARALLEL_API_KEY"],
});
async function main() {
const taskRun = await client.taskRun.create({
input:
"Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity and other relevant details.",
processor: "ultra",
});
console.log(`Run ID: ${taskRun.run_id}`);
// Poll for results with 25-second timeout, retry up to 144 times (1 hour total)
let runResult;
for (let i = 0; i < 144; i++) {
try {
runResult = await client.taskRun.result(taskRun.run_id, { timeout: 25 });
break;
} catch (error) {
if (i === 143) throw error; // Last attempt failed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(runResult.output);
}
main();
```
```bash cURL theme={"system"}
curl -X POST "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: $PARALLEL_API_KEY" \
-H 'Content-Type: application/json' \
--data-raw '{
"input": "Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity and other relevant details.",
"processor": "ultra"
}'
```
### Text Schema
Specifying text schema mode in the Task API output schema triggers Deep Research with a markdown report output format.
The generated result will contain extensive research formatted into a markdown report with in-line citations.
This format is perfect for human-readable content as well as LLM ingestion.
To provide guidance on the output, use the description field when specifying text schema. This
allows users to steer the report generated towards a certain direction like control over the length or the content
of the report.
```python Python theme={"system"}
from parallel import Parallel
from parallel.types import TaskSpecParam, TextSchemaParam
client = Parallel(api_key="PARALLEL_API_KEY")
task_run = client.task_run.create(
input="Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity and other relevant details.",
processor="ultra",
task_spec=TaskSpecParam(output_schema=TextSchemaParam())
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env["PARALLEL_API_KEY"],
});
async function main() {
const taskRun = await client.taskRun.create({
input:
"Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity and other relevant details.",
processor: "ultra",
task_spec: {
output_schema: {
type: "text",
},
},
});
console.log(`Run ID: ${taskRun.run_id}`);
// Poll for results with 25-second timeout, retry up to 144 times (1 hour total)
let runResult;
for (let i = 0; i < 144; i++) {
try {
runResult = await client.taskRun.result(taskRun.run_id, { timeout: 25 });
break;
} catch (error) {
if (i === 143) throw error; // Last attempt failed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(runResult.output);
}
main();
```
```bash cURL theme={"system"}
curl -X POST "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: $PARALLEL_API_KEY" \
-H 'Content-Type: application/json' \
--data-raw '{
"input": "Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity and other relevant details.",
"processor": "ultra",
"task_spec": {
"output_schema": {
"type": "text"
}
}
}'
```
### Sample Response
**Important**: The response below shows the **final completed result** after
Deep Research has finished. When you first create a task, you'll receive an
immediate response with `"status": "running"`. You'll need to poll the task or
use [webhooks](/task-api/webhooks) to get the final structured research output
shown below.
Below is a shortened sample response using the `auto` schema. The complete response contained 124 content fields, with 610 total citations for this Task.
```json [expandable] theme={"system"}
{
"output": {
"content": {
"market_size_and_forecast": {
"cagr": "6.9%",
"market_segment": "U.S. HVAC Systems",
"current_valuation": "USD 29.89 billion (2024)",
"forecasted_valuation": "USD 54.02 billion",
"forecast_period": "2025-2033"
},
"company_profiles": [
{
"company_name": "Carrier Global Corporation",
"stock_ticker": "CARR",
"revenue": "$22.5 billion (FY2024)",
"market_capitalization": "$63.698 billion (July 1, 2025)",
"market_position": "Global leader in intelligent climate and energy solutions",
"recent_developments": "Acquisition of Viessmann Climate Solutions for $13 billion"
},
{
"company_name": "Daikin Industries, Ltd.",
"stock_ticker": "DKILY",
"revenue": "¥4,752.3 billion (FY2024)",
"market_position": "Japan's leading HVAC manufacturer and top global player",
"recent_developments": "Multiple acquisitions to strengthen supply capabilities"
}
],
"recent_mergers_and_acquisitions": {
"acquiring_company": "Carrier Ventures",
"target_company": "ZutaCore",
"deal_summary": "Strategic investment in liquid cooling systems for data centers",
"date": "February 2025"
},
"growth_opportunities": "Data center cooling, building retrofits, electrification, healthcare applications, and enhanced aftermarket services",
"market_segmentation_analysis": {
"dominant_segment": "Residential",
"dominant_segment_share": "39.8% (in 2024)",
"fastest_growing_segment": "Commercial",
"fastest_growing_segment_cagr": "7.4% (from 2025 to 2033)"
},
"publicly_traded_hvac_companies": [
{
"company_name": "Carrier Global Corporation",
"stock_ticker": "CARR"
},
{
"company_name": "Daikin Industries, Ltd.",
"stock_ticker": "DKILY"
},
{
"company_name": "Johnson Controls International plc",
"stock_ticker": "JCI"
}
]
},
"basis": [
{
"field": "market_size_and_forecast.current_valuation",
"reasoning": "Market size data sourced from Grand View Research industry analysis report, which provides comprehensive market valuation for the U.S. HVAC systems market in 2024.",
"citations": [
{
"url": "https://www.grandviewresearch.com/industry-analysis/us-hvac-systems-market",
"excerpts": [
"The U.S. HVAC systems market size was estimated at USD 29.89 billion in 2024"
],
"title": "U.S. HVAC Systems Market Size, Share & Trends Analysis Report"
}
],
"confidence": "high"
},
{
"field": "company_profiles.0.revenue",
"reasoning": "Carrier Global Corporation's 2024 revenue figures are directly reported in their financial communications and investor relations materials.",
"citations": [
{
"url": "https://monexa.ai/blog/carrier-global-corporation-strategic-climate-pivot-CARR-2025-07-02",
"excerpts": [
"Carrier reported **2024 revenues of $22.49 billion**, a modest increase of +1.76% year-over-year"
],
"title": "Carrier Global Corporation: Strategic Climate Pivot"
}
],
"confidence": "high"
},
{
"field": "recent_mergers_and_acquisitions",
"reasoning": "Carrier Ventures' strategic investment in ZutaCore represents recent M&A activity focused on next-generation cooling technologies for data centers.",
"citations": [
{
"url": "https://finance.yahoo.com/news/10-biggest-hvac-companies-usa-142547989.html",
"excerpts": [
"Strategic investment activity by Carrier Ventures in companies specializing in liquid cooling systems"
],
"title": "10 Biggest HVAC Companies in the USA"
}
],
"confidence": "medium"
}
],
"run_id": "trun_646e167d826747e1b4690e58d2b9941e",
"status": "completed",
"created_at": "2025-01-30T20:12:18.123456Z",
"completed_at": "2025-01-30T20:25:41.654321Z",
"processor": "ultra",
"warnings": null,
"error": null,
"taskgroup_id": null
}
}
```
Deep Research returns a response which includes the `content` and the `basis`, as with other [Task API](/task-api/guides/execute-task-run) executions. The key difference is that the `basis` object in an `auto` mode output contains Nested FieldBasis.
### Nested FieldBasis
In `text` mode, FieldBasis is not nested. It contains a list of citations (with
URLs and excerpts) for all sites visited during research. The most relevant citations
are included at the base of the report itself, with inline references.
In `auto` mode, the [Basis](/task-api/guides/access-research-basis) object maps each output field (including nested fields) with supporting evidence. This ensures that every output, including nested output fields, has citations, excerpts, confidence levels and reasoning.
For nested fields, the basis uses dot notation for indexing:
* `key_players.0` for the first item in a key players array
* `industry_overview.growth_cagr` for nested object fields
* `market_trends.2.description` for nested arrays with objects
## Example: Market Research Assistant
Here's how to build a market research tool with Deep Research, showing different approaches for handling the async nature of the Task API:
```python Basic Implementation theme={"system"}
from parallel import Parallel
client = Parallel(api_key="PARALLEL_API_KEY")
# Execute research task (handles polling internally)
task_run = client.task_run.create(
input="Create a comprehensive market research report on the renewable energy storage market in Europe, focusing on battery technologies and policy impacts",
processor="ultra"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(f"Research completed! Output has {len(run_result.output.basis)} structured fields")
for field in run_result.output.basis[:3]:
print(f"- {field.field}: {len(field.citations)} citations")
```
```typescript Basic Implementation theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env["PARALLEL_API_KEY"],
});
// Execute research task
const taskRun = await client.taskRun.create({
input:
"Create a comprehensive market research report on the renewable energy storage market in Europe, focusing on battery technologies and policy impacts",
processor: "ultra",
});
console.log(`Run ID: ${taskRun.run_id}`);
// Poll for results with 25-second timeout, retry up to 144 times (1 hour total)
let runResult;
for (let i = 0; i < 144; i++) {
try {
runResult = await client.taskRun.result(taskRun.run_id, { timeout: 25 });
break;
} catch (error) {
if (i === 143) throw error; // Last attempt failed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(
`Research completed! Output has ${runResult.output.basis.length} structured fields`
);
runResult.output.basis.slice(0, 3).forEach((field) => {
console.log(`- ${field.field}: ${field.citations?.length || 0} citations`);
});
```
```python With Polling theme={"system"}
from parallel import Parallel
import time
client = Parallel(api_key="PARALLEL_API_KEY")
# Create the research task (low-level API)
task_run = client.task_run.create(
input="Create a comprehensive market research report on the renewable energy storage market in Europe, focusing on battery technologies and policy impacts",
processor="ultra"
)
print(f"Task created: {task_run.run_id}")
print("Polling for completion...")
# Manual polling for completion (Deep Research can take up to 15 minutes)
while True:
status = client.task_run.retrieve(task_run.run_id)
print(f"Status: {status.status}")
if status.status == "completed":
# Get the final results using the result() method
run_result = client.task_run.result(task_run.run_id)
print(f"Research completed! Output has {len(run_result.output.basis)} structured fields")
# Display sample findings
for field in run_result.output.basis[:3]:
print(f"- {field.field}: {len(field.citations)} citations")
break
elif status.status == "failed":
print("Task failed")
break
time.sleep(60) # Check every 60 seconds
```
```typescript With Polling theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env["PARALLEL_API_KEY"],
});
async function main() {
// Create the research task (low-level API)
const taskRun = await client.taskRun.create({
input:
"Create a comprehensive market research report on the renewable energy storage market in Europe, focusing on battery technologies and policy impacts",
processor: "ultra",
});
console.log(`Task created: ${taskRun.run_id}`);
console.log("Polling for completion...");
// Manual polling for completion (Deep Research can take up to 1 hour)
let attempts = 0;
const maxAttempts = 144; // 144 * 25 seconds = 1 hour
while (attempts < maxAttempts) {
const status = await client.taskRun.retrieve(taskRun.run_id);
console.log(
`Status: ${status.status} (attempt ${attempts + 1}/${maxAttempts})`
);
if (status.status === "completed") {
// Get the final results using the result() method
const runResult = await client.taskRun.result(taskRun.run_id, {
timeout: 25,
});
console.log(
`Research completed! Output has ${runResult.output.basis.length} structured fields`
);
// Display sample findings
runResult.output.basis.slice(0, 3).forEach((field) => {
console.log(
`- ${field.field}: ${field.citations?.length || 0} citations`
);
});
break;
} else if (status.status === "failed") {
console.log("Task failed");
break;
}
attempts++;
await new Promise((resolve) => setTimeout(resolve, 25000)); // Check every 25 seconds
}
if (attempts >= maxAttempts) {
console.log("Task timed out after 1 hour");
}
}
main();
```
## Polling vs Webhooks vs SSE
The Task API is asynchronous—when you create a task, it returns immediately with a `run_id` while processing continues in the background. There are three ways to get results:
| Method | What It Does | Best For |
| - | - | - |
| **Polling** | Your code repeatedly calls the API to check if the task is done | Simple integrations, scripts, testing |
| **Webhooks** | Parallel sends an HTTP request to your server when the task completes | Production apps with backend servers |
| **SSE** | Stream real-time progress updates as the task runs | Interactive UIs, monitoring progress |
### Polling
**How it works:** After creating a task, repeatedly check its status until it completes.
```python theme={"system"}
import time
# Create task
task_run = client.task_run.create(input="...", processor="ultra")
# Poll until complete
while True:
status = client.task_run.retrieve(task_run.run_id)
if status.status == "completed":
break
if status.status == "failed":
raise Exception(f"Task failed: {status.error}")
time.sleep(5) # Wait 5 seconds between checks
# Get the result
result = client.task_run.result(task_run.run_id)
```
**Key points:**
* Simplest approach—no infrastructure needed
* Use `retrieve()` to check status, then `result()` when complete
* The `result()` method also blocks until complete if you prefer a one-liner: `client.task_run.result(run_id, api_timeout=3600)`
### Webhooks
**How it works:** Provide a webhook URL when creating the task. Parallel sends a POST request to your URL when the task finishes.
```python theme={"system"}
task_run = client.task_run.create(
input="...",
processor="ultra",
webhook={
"url": "https://your-server.com/webhooks/parallel",
"event_types": ["task_run.status"]
},
)
```
**Key points:**
* Webhooks notify you when the task **completes**—they don't send the actual results
* After receiving the webhook, call `result()` to retrieve the output data
* Requires a publicly accessible HTTPS endpoint
* See [Webhooks documentation](/task-api/webhooks) for setup and verification
**Important:** Webhooks are a notification mechanism, not a data delivery mechanism. The webhook payload contains the task status and metadata, but you must make a separate API call to retrieve the actual research results.
### Server-Sent Events (SSE)
**How it works:** Connect to a streaming endpoint to receive real-time progress updates as the task runs.
```bash theme={"system"}
# First, create task with events enabled
curl -X POST "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: $PARALLEL_API_KEY" \
-d '{"input": "...", "processor": "ultra", "enable_events": true}'
# Then connect to the event stream
curl "https://api.parallel.ai/v1/tasks/runs/{run_id}/events" \
-H "x-api-key: $PARALLEL_API_KEY"
```
**Key points:**
* See real-time progress: research plan, sources being explored, intermediate findings
* The final `task_run.state` event includes the complete output
* Ideal for showing users what's happening during long research tasks
* See [Streaming Events documentation](/task-api/task-sse) for event types and examples
### Which Should I Use?
| Scenario | Recommended Method |
| - | - |
| Testing or one-off scripts | Polling |
| Backend service processing many tasks | Webhooks |
| User-facing app showing research progress | SSE |
| Simple integration without a server | Polling |
| Production system needing reliability | Webhooks + Polling fallback |
## Example: Chatting with Deep Research
You can use [interactions](/task-api/guides/interactions) to ask follow-up questions about a Deep Research result. The initial research runs with a `pro` or `ultra` processor, and each follow-up automatically has the full context of the original report — so you can drill into specific findings without re-running the entire research.
```python Python theme={"system"}
from parallel import Parallel
from parallel.types import TaskSpecParam, TextSchemaParam
client = Parallel(api_key="PARALLEL_API_KEY")
# Step 1: Run deep research
task_run = client.task_run.create(
input="Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity.",
processor="ultra",
)
result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(f"Research complete: {len(result.output.basis)} fields with citations\n")
# Step 2: Ask follow-up questions using interaction chaining
followup1 = client.task_run.create(
input="Which of the M&A deals you found has the largest deal value?",
processor="core",
previous_interaction_id=task_run.interaction_id,
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
)
followup1_result = client.task_run.result(followup1.run_id, api_timeout=3600)
print(f"Follow-up 1: {followup1_result.output.content}\n")
# Step 3: Drill deeper into the same thread
followup2 = client.task_run.create(
input="What was the strategic rationale behind that acquisition?",
processor="core",
previous_interaction_id=followup1.interaction_id,
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
)
followup2_result = client.task_run.result(followup2.run_id, api_timeout=3600)
print(f"Follow-up 2: {followup2_result.output.content}")
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
async function pollResult(runId: string) {
for (let i = 0; i < 144; i++) {
try {
return await client.taskRun.result(runId, { timeout: 25 });
} catch (error) {
if (i === 143) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
// Step 1: Run deep research
const taskRun = await client.taskRun.create({
input:
"Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity.",
processor: "ultra",
});
const result = await pollResult(taskRun.run_id);
console.log(
`Research complete: ${result.output.basis.length} fields with citations\n`
);
// Step 2: Ask follow-up questions using interaction chaining
const followup1 = await client.taskRun.create({
input: "Which of the M&A deals you found has the largest deal value?",
processor: "core",
previous_interaction_id: taskRun.interaction_id,
task_spec: { output_schema: { type: "text" } },
});
const followup1Result = await pollResult(followup1.run_id);
console.log(`Follow-up 1: ${followup1Result.output.content}\n`);
// Step 3: Drill deeper into the same thread
const followup2 = await client.taskRun.create({
input: "What was the strategic rationale behind that acquisition?",
processor: "core",
previous_interaction_id: followup1.interaction_id,
task_spec: { output_schema: { type: "text" } },
});
const followup2Result = await pollResult(followup2.run_id);
console.log(`Follow-up 2: ${followup2Result.output.content}`);
```
```bash cURL theme={"system"}
# Step 1: Run deep research
echo "Running deep research..."
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"input": "Create a comprehensive market research report on the HVAC industry in the USA including an analysis of recent M&A activity.",
"processor": "ultra"
}')
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
INTERACTION_ID=$(echo "$RUN_JSON" | jq -r '.interaction_id')
# Poll for deep research result
RESULT=$(curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}")
echo "Research complete."
# Step 2: Follow-up question using interaction chaining
echo "Follow-up 1: Which M&A deal has the largest deal value?"
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"input\": \"Which of the M&A deals you found has the largest deal value?\",
\"processor\": \"core\",
\"previous_interaction_id\": \"${INTERACTION_ID}\",
\"task_spec\": { \"output_schema\": { \"type\": \"text\" } }
}")
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
INTERACTION_ID=$(echo "$RUN_JSON" | jq -r '.interaction_id')
RESULT=$(curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}")
echo "$RESULT" | jq -r .output.content
# Step 3: Drill deeper into the same thread
echo "Follow-up 2: What was the strategic rationale?"
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"input\": \"What was the strategic rationale behind that acquisition?\",
\"processor\": \"core\",
\"previous_interaction_id\": \"${INTERACTION_ID}\",
\"task_spec\": { \"output_schema\": { \"type\": \"text\" } }
}")
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
RESULT=$(curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}")
echo "$RESULT" | jq -r .output.content
```
Follow-up questions use `core` processor for faster responses since they build on context already gathered by the initial deep research. See [Interactive Research](/task-api/examples/interactive-research) for a full multi-turn chat loop pattern.
## Next Steps
* [**Interactive Research:**](/task-api/examples/interactive-research) Build a multi-turn research chat using interaction chaining
* [**Choose a Processor:**](/task-api/guides/choose-a-processor) Deep Research works best with `pro` or `ultra` processors—for latency-sensitive workflows, use the [Responses API](/responses-api/responses-quickstart)
* [**Task Spec Best Practices:**](/task-api/guides/specify-a-task) Craft effective research queries and output specifications
* [**Task Groups:**](/task-api/group-api) Run multiple research queries in parallel for batch intelligence gathering
* [**Access Research Basis:**](/task-api/guides/access-research-basis) Understand nested FieldBasis structure for auto schema outputs
* [**Streaming Events:**](/task-api/task-sse) Monitor long-running research tasks with real-time progress updates
* [**Webhooks:**](/task-api/webhooks) Configure HTTP callbacks for research completion notifications
* [**Enrichment:**](/task-api/examples/task-enrichment) Learn about enriching structured data instead of open-ended research
* [**API Reference:**](/api-reference/tasks/create-task-run) Complete endpoint documentation for the Task API
## Rate Limits
See [Rate Limits](/getting-started/rate-limits) for default quotas and how to request higher limits.
# Task API Enrichment Quickstart
Source: https://docs.parallel.ai/task-api/examples/task-enrichment
Enrich your structured data with web intelligence using the Task API
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## What is Enrichment?
Enrichment is when you have existing structured data—like a list of companies, products, or contacts—and want to enhance it with additional information from the web. The Task API makes it easy to define what data you have and what additional fields you need, then automatically researches and populates those fields at scale.
## How Enrichment Works
With enrichment, you define two schemas:
1. **Input Schema**: The data fields you already have (e.g., company name, website)
2. **Output Schema**: The new fields you want to add (e.g., employee count, funding sources, founding date)
The Task API researches the web and populates your output fields with accurate, cited information.
## 1. Set up Prerequisites
Generate your API key on [Platform](https://platform.parallel.ai). Then, set up with the TypeScript SDK, Python SDK or with cURL:
```bash cURL theme={"system"}
echo "Install curl and jq via brew, apt, or your favorite package manager"
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
```bash Python theme={"system"}
pip install parallel-web
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
```bash TypeScript theme={"system"}
npm install parallel-web
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
```bash Python (Async) theme={"system"}
pip install parallel-web
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
## 2. Execute your First Enrichment Task
Let's enrich a simple company record. We'll start with just a company name and enrich it with a founding date:
You can learn about our available Processors [here →](/task-api/guides/choose-a-processor)
```bash cURL theme={"system"}
echo "Creating the run:"
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"task_spec": {
"output_schema": "The founding date of the company in the format MM-YYYY"
},
"input": "United Nations",
"processor": "base"
}')
echo "$RUN_JSON" | jq .
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
echo "Retrieving the run result, blocking until the result is available:"
curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}" | jq .
```
```python Python theme={"system"}
import os
from parallel import Parallel
from parallel.types import TaskSpecParam
client = Parallel(api_key=os.environ["PARALLEL_API_KEY"])
task_run = client.task_run.create(
input="United Nations",
task_spec=TaskSpecParam(
output_schema="The founding date of the company in the format MM-YYYY"
),
processor="base"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
const taskRun = await client.taskRun.create({
input: "United Nations",
task_spec: {
output_schema: "The founding date of the company in the format MM-YYYY",
},
processor: "base",
});
console.log(`Run ID: ${taskRun.run_id}`);
// Poll for results with 25-second timeout, retry up to 144 times (1 hour total)
let runResult;
for (let i = 0; i < 144; i++) {
try {
runResult = await client.taskRun.result(taskRun.run_id, { timeout: 25 });
break;
} catch (error) {
if (i === 143) throw error; // Last attempt failed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(runResult.output);
```
```python Python (Async) theme={"system"}
import asyncio
import os
from parallel import AsyncParallel
from parallel.types import TaskSpecParam
client = AsyncParallel(api_key=os.environ["PARALLEL_API_KEY"])
async def run_task():
task_run = await client.task_run.create(
input="United Nations",
task_spec=TaskSpecParam(
output_schema="The founding date of the company in the format MM-YYYY"
),
processor="base"
)
print(f"Run ID: {task_run.run_id}")
run_result = await client.task_run.result(task_run.run_id, api_timeout=3600)
return run_result
run_result = asyncio.run(run_task())
print(run_result.output)
```
### Sample Response
Immediately after a Task Run is created, the Task Run object, including the status of the Task Run, is returned. On completion, the Task Run Result object is returned.
[Basis](/task-api/guides/access-research-basis), including citations, reasoning, confidence, and excerpts - is returned with every Task Run Result.
```json Task Run Creation theme={"system"}
{
"run_id": "trun_9907962f83aa475e8323da43b325088c",
"interaction_id": "trun_9907962f83aa475e8323da43b325088c",
"status": "queued",
"is_active": true,
"processor": "base",
"metadata": {},
"created_at": "2026-09-30T18:35:45.052993Z",
"modified_at": "2026-09-30T18:35:45.053248Z"
}
```
```json Task Run Result [expandable] theme={"system"}
{
"run": {
"run_id": "trun_9907962f83aa475e8323da43b325088c",
"interaction_id": "trun_9907962f83aa475e8323da43b325088c",
"status": "completed",
"is_active": false,
"processor": "base",
"metadata": {},
"created_at": "2026-09-30T18:35:45.052993Z",
"modified_at": "2026-09-30T18:35:53.283820Z"
},
"output": {
"basis": [
{
"field": "output",
"citations": [
{
"title": "History of the United Nations | United Nations",
"url": "https://www.un.org/en/about-us/history-of-the-un",
"excerpts": [
"Four months after the San Francisco Conference ended, the United Nations officially began, on 24 October 1945, when it came into existence after its Charter had been ratified by China, France, the ..."
]
}
],
"reasoning": "The United Nations’ official history states that it officially began on 24 October 1945. That date corresponds to 10-1945.",
"confidence": "high"
}
],
"type": "text",
"content": "10-1945"
}
}
```
## 3. From Simple to Rich Enrichment
The Task API supports increasingly sophisticated enrichment patterns:
The simplest enrichment: take one piece of data (like a company name) and add one new field (like founding date). This straightforward approach is illustrated above.
Enrich a single input field with multiple new data points. For example, pass in a company name and receive founding date, employee count, and funding sources.
```bash cURL [expandable] theme={"system"}
echo "Creating the run:"
RUN_JSON=$(curl -s 'https://api.parallel.ai/v1/tasks/runs' \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H 'Content-Type: application/json' \
-d '{
"input": "United Nations",
"processor": "core",
"task_spec": {
"output_schema": {
"type": "json",
"json_schema": {
"type": "object",
"properties": {
"founding_date": {
"type": "string",
"description": "The official founding date of the company in the format MM-YYYY"
},
"employee_count": {
"type": "string",
"enum": [
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"501-1000 employees",
"1001-5000 employees",
"5001-10000 employees",
"10001+ employees"
],
"description": "The range of employees working at the company. Choose the most accurate range possible and make sure to validate across multiple sources."
},
"funding_sources": {
"type": "string",
"description": "A detailed description, containing 1-4 sentences, of the company's funding sources, including their estimated value."
}
},
"required": ["founding_date", "employee_count", "funding_sources"],
"additionalProperties": false
}
}
}
}')
echo "$RUN_JSON" | jq .
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
echo "Retrieving the run result, blocking until the result is available:"
curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}" | jq .
```
```typescript TypeScript [expandable] theme={"system"}
import Parallel from 'parallel-web';
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
const taskRun = await client.taskRun.create({
input: 'United Nations',
processor: 'core',
task_spec: {
output_schema: {
type: 'json',
json_schema: {
type: 'object',
properties: {
founding_date: {
type: 'string',
description: 'The official founding date of the company in the format MM-YYYY',
},
employee_count: {
type: 'string',
enum: [
'1-10 employees',
'11-50 employees',
'51-200 employees',
'201-500 employees',
'501-1000 employees',
'1001-5000 employees',
'5001-10000 employees',
'10001+ employees',
],
description: 'The range of employees working at the company. Choose the most accurate range possible and make sure to validate across multiple sources.',
},
funding_sources: {
type: 'string',
description: "A detailed description, containing 1-4 sentences, of the company's funding sources, including their estimated value.",
},
},
required: ['founding_date', 'employee_count', 'funding_sources'],
additionalProperties: false,
},
},
},
});
console.log(`Run ID: ${taskRun.run_id}`);
// Poll for results with 25-second timeout, retry up to 144 times (1 hour total)
let runResult;
for (let i = 0; i < 144; i++) {
try {
runResult = await client.taskRun.result(taskRun.run_id, { timeout: 25 });
break;
} catch (error) {
if (i === 143) throw error; // Last attempt failed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(runResult.output);
```
```python Python [expandable] theme={"system"}
import os
from parallel import Parallel
from pydantic import BaseModel, Field
from typing import Literal
class CompanyOutput(BaseModel):
founding_date: str = Field(
description="The official founding date of the company in the format MM-YYYY"
)
employee_count: Literal[
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"501-1000 employees",
"1001-5000 employees",
"5001-10000 employees",
"10001+ employees"
] = Field(
description="The range of employees working at the company. Choose the most accurate range possible and make sure to validate across multiple sources."
)
funding_sources: str = Field(
description="A detailed description, containing 1-4 sentences, of the company's funding sources, including their estimated value."
)
def main():
client = Parallel(api_key="PARALLEL_API_KEY")
task_run = client.task_run.create(
input="United Nations",
task_spec={
"output_schema":{
"type":"json",
"json_schema":CompanyOutput.model_json_schema()
}
},
processor="core"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
if __name__ == "__main__":
main()
```
```python Python (Async) [expandable] theme={"system"}
import asyncio
import os
from parallel import AsyncParallel
from pydantic import BaseModel, Field
from typing import Literal
class CompanyOutput(BaseModel):
founding_date: str = Field(
description="The official founding date of the company in the format MM-YYYY"
)
employee_count: Literal[
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"501-1000 employees",
"1001-5000 employees",
"5001-10000 employees",
"10001+ employees"
] = Field(
description="The range of employees working at the company. Choose the most accurate range possible and make sure to validate across multiple sources."
)
funding_sources: str = Field(
description="A detailed description, containing 1-4 sentences, of the company's funding sources, including their estimated value."
)
async def main():
client = AsyncParallel(api_key="PARALLEL_API_KEY")
task_run = await client.task_run.create(
input="United Nations",
task_spec={
"output_schema":{
"type":"json",
"json_schema":CompanyOutput.model_json_schema()
}
},
processor="core"
)
print(f"Run ID: {task_run.run_id}")
run_result = await client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
if __name__ == "__main__":
asyncio.run(main())
```
The full enrichment pattern: define both input and output schemas. Provide multiple data fields you already have (company name and website) and specify all the fields you want to enrich. This is the most common pattern for enriching CRM data, compliance checks, and other structured workflows.
```bash cURL [expandable] theme={"system"}
echo "Creating the run:"
RUN_JSON=$(curl -s 'https://api.parallel.ai/v1/tasks/runs' \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H 'Content-Type: application/json' \
-d '{
"input": {
"company_name": "United Nations",
"company_website": "www.un.org"
},
"processor": "core",
"task_spec": {
"output_schema": {
"type": "json",
"json_schema": {
"type": "object",
"properties": {
"founding_date": {
"type": "string",
"description": "The official founding date of the company in the format MM-YYYY"
},
"employee_count": {
"type": "string",
"enum":[
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"501-1000 employees",
"1001-5000 employees",
"5001-10000 employees",
"10001+ employees"
],
"description": "The range of employees working at the company. Choose the most accurate range possible and make sure to validate across multiple sources."
},
"funding_sources": {
"type": "string",
"description": "A detailed description, containing 1-4 sentences, of the company's funding sources, including their estimated value."
}
},
"required": ["founding_date", "employee_count", "funding_sources"],
"additionalProperties": false
}
},
"input_schema": {
"type": "json",
"json_schema": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"description": "The name of the company to research"
},
"company_website": {
"type": "string",
"description": "The website of the company to research"
}
},
"required": ["company_name", "company_website"]
}
}
}
}')
echo "$RUN_JSON" | jq .
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
echo "Retrieving the run result, blocking until the result is available:"
curl -s "https://api.parallel.ai/v1/tasks/runs/${RUN_ID}/result" \
-H "x-api-key: ${PARALLEL_API_KEY}" | jq .
```
```typescript TypeScript [expandable] theme={"system"}
import Parallel from 'parallel-web';
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
// Define input and output schemas
const inputSchema = {
type: 'object' as const,
properties: {
company_name: {
type: 'string' as const,
description: 'The name of the company to research',
},
company_website: {
type: 'string' as const,
description: 'The website of the company to research',
},
},
required: ['company_name', 'company_website'],
};
const outputSchema = {
type: 'object' as const,
properties: {
founding_date: {
type: 'string' as const,
description: 'The official founding date of the company in the format MM-YYYY',
},
employee_count: {
type: 'string' as const,
enum: [
'1-10 employees',
'11-50 employees',
'51-200 employees',
'201-500 employees',
'501-1000 employees',
'1001-5000 employees',
'5001-10000 employees',
'10001+ employees',
],
description: 'The range of employees working at the company. Choose the most accurate range possible and validate across multiple sources.',
},
funding_sources: {
type: 'string' as const,
description: "A detailed description, containing 1–4 sentences, of the company's funding sources, including their estimated value.",
},
},
required: ['founding_date', 'employee_count', 'funding_sources'],
additionalProperties: false,
};
const taskRun = await client.taskRun.create({
input: {
company_name: 'United Nations',
company_website: 'www.un.org',
},
processor: 'core',
task_spec: {
input_schema: {
type: 'json',
json_schema: inputSchema,
},
output_schema: {
type: 'json',
json_schema: outputSchema,
},
},
});
console.log(`Run ID: ${taskRun.run_id}`);
// Poll for results with 25-second timeout, retry up to 144 times (1 hour total)
let runResult;
for (let i = 0; i < 144; i++) {
try {
runResult = await client.taskRun.result(taskRun.run_id, { timeout: 25 });
break;
} catch (error) {
if (i === 143) throw error; // Last attempt failed
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(runResult.output);
```
```python Python [expandable] theme={"system"}
import os
from typing import Literal
from parallel import Parallel
from parallel.lib._parsing._task_run_result import task_run_result_parser
from parallel.types import TaskSpecParam
from pydantic import BaseModel, Field
class CompanyInput(BaseModel):
"""Input schema for the company research task."""
company_name: str = Field(description="The name of the company to research")
company_website: str = Field(description="The website of the company to research")
class CompanyOutput(BaseModel):
"""Output schema for the company research task."""
founding_date: str = Field(
description="The official founding date of the company in the format MM-YYYY"
)
employee_count: Literal[
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"501-1000 employees",
"1001-5000 employees",
"5001-10000 employees",
"10001+ employees",
] = Field(
description="The range of employees working at the company. Choose the most accurate range possible and validate across multiple sources."
)
funding_sources: str = Field(
description="A detailed description, containing 1–4 sentences, of the company's funding sources, including their estimated value."
)
def build_task_spec_param(
input_schema: type[BaseModel], output_schema: type[BaseModel]
) -> TaskSpecParam:
"""Build a TaskSpecParam from an input and output schema."""
return {
"input_schema": {
"type": "json",
"json_schema": input_schema.model_json_schema(),
},
"output_schema": {
"type": "json",
"json_schema": output_schema.model_json_schema(),
},
}
client = Parallel(api_key=os.environ.get("PARALLEL_API_KEY"))
# Prepare structured input
input_data = CompanyInput(
company_name="United Nations", company_website="htt"
)
task_spec = build_task_spec_param(CompanyInput, CompanyOutput)
task_run = client.task_run.create(
input=input_data.model_dump(),
task_spec=task_spec,
processor="core",
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
parsed_result = task_run_result_parser(run_result, CompanyOutput)
print(parsed_result.output.parsed)
```
```python Python (Async) [expandable] theme={"system"}
import asyncio
import os
from typing import Literal
from parallel import AsyncParallel
from parallel.lib._parsing._task_run_result import task_run_result_parser
from parallel.types import TaskSpecParam
from pydantic import BaseModel, Field
class CompanyInput(BaseModel):
"""Input schema for the company research task."""
company_name: str = Field(description="The name of the company to research")
company_website: str = Field(description="The website of the company to research")
class CompanyOutput(BaseModel):
"""Output schema for the company research task."""
founding_date: str = Field(
description="The official founding date of the company in the format MM-YYYY"
)
employee_count: Literal[
"1-10 employees",
"11-50 employees",
"51-200 employees",
"201-500 employees",
"501-1000 employees",
"1001-5000 employees",
"5001-10000 employees",
"10001+ employees",
] = Field(
description="The range of employees working at the company. Choose the most accurate range possible and validate across multiple sources."
)
funding_sources: str = Field(
description="A detailed description, containing 1–4 sentences, of the company's funding sources, including their estimated value."
)
def build_task_spec_param(
input_schema: type[BaseModel], output_schema: type[BaseModel]
) -> TaskSpecParam:
"""Build a TaskSpecParam from an input and output schema."""
return {
"input_schema": {
"type": "json",
"json_schema": input_schema.model_json_schema(),
},
"output_schema": {
"type": "json",
"json_schema": output_schema.model_json_schema(),
},
}
async def main():
# Initialize the Parallel client
client = AsyncParallel(api_key="PARALLEL_API_KEY")
# Prepare structured input
input_data = CompanyInput(
company_name="United Nations", company_website="www.un.org"
)
task_spec = build_task_spec_param(CompanyInput, CompanyOutput)
task_run = await client.task_run.create(
input=input_data.model_dump(),
task_spec=task_spec,
processor="core",
)
print(f"Run ID: {task_run.run_id}")
run_result = await client.task_run.result(task_run.run_id, api_timeout=3600)
parsed_result = task_run_result_parser(run_result, CompanyOutput)
print(parsed_result.output.parsed)
if __name__ == "__main__":
asyncio.run(main())
```
**Writing Effective Task Specs**: For best practices on defining input and output schemas that produce high-quality results, see our [Task Spec Best Practices guide](/task-api/guides/specify-a-task#task-spec-best-practices).
### Sample Enrichment Result
```json [expandable] theme={"system"}
{
"run": {
"run_id": "trun_0824bb53c79c407b89614ba22e9db51c",
"status": "completed",
"is_active": false,
"warnings": [],
"processor": "core",
"metadata": null,
"created_at": "2025-04-24T16:05:03.403102Z",
"modified_at": "2025-04-24T16:05:33.099450Z"
},
"output": {
"content": {
"funding_sources": "The United Nations' funding comes from governments, multilateral partners, and other non-state entities. This funding is acquired through assessed and voluntary contributions from its member states.",
"employee_count": "10001+ employees",
"founding_date": "10-1945"
},
"basis": [
{
"field": "funding_sources",
"citations": [
{
"title": "Funding sources",
"url": "https://www.financingun.report/un-financing/un-funding/funding-entity",
"excerpts": [
"The UN system is funded by a diverse set of partners: governments, multilateral partners, and other non-state funding."
]
},
{
"title": "US Funding for the UN",
"url": "https://betterworldcampaign.org/us-funding-for-the-un",
"excerpts": [
"Funding from Member States for the UN system comes from two main sources: assessed and voluntary contributions."
]
}
],
"reasoning": "The United Nations' funding is derived from a diverse set of partners, including governments, multilateral organizations, and other non-state entities, as stated by financingun.report. According to betterworldcampaign.org, the funding from member states is acquired through both assessed and voluntary contributions.",
"confidence": "high"
},
{
"field": "employee_count",
"citations": [
{
"title": "Funding sources",
"url": "https://www.financingun.report/un-financing/un-funding/funding-entity",
"excerpts": []
}
],
"reasoning": "The UN employs approximately 37,000 people, with a total personnel count of 133,126 in 2023.",
"confidence": "low"
},
{
"field": "founding_date",
"citations": [
{
"title": "Funding sources",
"url": "https://www.financingun.report/un-financing/un-funding/funding-entity",
"excerpts": []
},
{
"title": "History of the United Nations",
"url": "https://www.un.org/en/about-us/history-of-the-un",
"excerpts": [
"The United Nations officially began, on 24 October 1945, when it came into existence after its Charter had been ratified by China, France, the Soviet Union, ..."
]
},
{
"title": "The Formation of the United Nations, 1945",
"url": "https://history.state.gov/milestones/1937-1945/un",
"excerpts": [
"The United Nations came into existence on October 24, 1945, after 29 nations had ratified the Charter. Table of Contents. 1937–1945: Diplomacy and the Road to ..."
]
}
],
"reasoning": "The United Nations officially began on October 24, 1945, as stated in multiple sources including the UN's official history and the US Department of State's historical milestones. This date is when the UN came into existence after its Charter was ratified by key member states.",
"confidence": "high"
}
],
"type": "json"
}
}
```
## Next Steps
* [**Task Groups:**](/task-api/group-api) Enrich multiple records concurrently with parallel execution and batch tracking
* [**Task Spec Best Practices:**](/task-api/guides/specify-a-task) Optimize your input and output schemas for accuracy and speed
* [**Choose a Processor:**](/task-api/guides/choose-a-processor) Select the right processor tier for your enrichment use case
* [**Access Research Basis:**](/task-api/guides/access-research-basis) Understand citations, confidence levels, and reasoning for every enriched field
* [**Deep Research:**](/task-api/examples/task-deep-research) Explore open-ended research without structured input data
* [**Streaming Events:**](/task-api/task-sse) Receive real-time updates via Server-Sent Events for long-running enrichments
* [**Webhooks:**](/task-api/webhooks) Configure HTTP callbacks for task completion notifications
* [**API Reference:**](/api-reference/tasks/create-task-run) Complete endpoint documentation for the Task API
## Rate Limits
See [Rate Limits](/getting-started/rate-limits) for default quotas and how to request higher limits.
# Task Group
Source: https://docs.parallel.ai/task-api/group-api
Batch process Tasks at scale with the Parallel Task Group API
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
The Parallel Task Group API enables you to batch process hundreds or thousands of Tasks efficiently. Instead of running Tasks one by one, you can organize them into groups, monitor their progress collectively, and retrieve results in bulk. The API is comprised of the following endpoints:
**Creation**: To run a batch of tasks in a group, you first need to create a task group, after which you can add runs to it, which will be queued and processed.
* `POST /v1/tasks/groups` (Create task-group)
* `POST /v1/tasks/groups/{taskgroup_id}/runs` (Add runs. Up to 1,000 runs per POST request.)
**Progress Snapshot**: At any moment during the task, you can get an instant snapshot of the state of it using `GET /{taskgroup_id}` and `GET /{taskgroup_id}/runs`. The runs endpoint streams back the requested runs immediately using SSE to support large payloads without pagination; it does not wait for runs to complete. Task Group runs are subject to your account's data-retention configuration. Persist any results your application needs after processing; [Zero Data Retention (ZDR)](/getting-started/glossary) deployments do not retain completed run data.
* `GET /v1/tasks/groups/{taskgroup_id}` (Get task-group summary)
* `GET /v1/tasks/groups/{taskgroup_id}/runs` (Fetch task group runs)
* `GET /v1/tasks/groups/{taskgroup_id}/runs/{run_id}` (Retrieve one task run's status)
**Realtime updates**: You may want to provide efficient real-time updates to your app. For group status updates and non-active run state events, you can use `GET /{taskgroup_id}/events`. To retrieve a completed task run's result, use the [task run result endpoint](/api-reference/tasks/retrieve-task-run-result).
* `GET /v1/tasks/groups/{taskgroup_id}/events` (Stream task-group events)
* `GET /v1/tasks/runs/{run_id}/result` (Get task-run result)
To determine when a task group has no active runs, you can either use realtime update events or poll the task-group summary endpoint. You can continue adding runs to a task group after it becomes inactive.
## Key Concepts
### Task Groups
A Task Group is a container that organizes multiple task runs. Each group has:
* A unique `taskgroup_id` for identification
* A status object with `is_active` (boolean) and `task_run_status_counts` (counts by status)
* The ability to add new Tasks dynamically
### Group Status
Track progress with real-time status updates:
* Total number of task runs
* Count of runs by status (`queued`, `action_required`, `running`, `completed`, `failed`, `cancelling`, or `cancelled`)
* Whether the group is active (`is_active` is `true` while any run is `queued`, `running`, or `cancelling`)
An inactive group has no active runs, but its runs may have completed, failed, been cancelled, or require action. Inspect `task_run_status_counts` to distinguish those outcomes.
## Quick Start
### 1. Define Types and Task Structure
```bash cURL theme={"system"}
# Define task specification as a variable
TASK_SPEC='{
"input_schema": {
"json_schema": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"description": "Name of the company"
},
"company_website": {
"type": "string",
"description": "Company website URL"
}
},
"required": ["company_name", "company_website"]
}
},
"output_schema": {
"json_schema": {
"type": "object",
"properties": {
"key_insights": {
"type": "array",
"items": {"type": "string"},
"description": "Key business insights"
},
"market_position": {
"type": "string",
"description": "Market positioning analysis"
}
},
"required": ["key_insights", "market_position"]
}
}
}'
```
```python Python theme={"system"}
import pydantic
from parallel import AsyncParallel
from parallel.types import TaskSpecParam, JsonSchemaParam
from parallel.types.run_input_param import RunInputParam
# Define your input and output models
class CompanyInput(pydantic.BaseModel):
company_name: str = pydantic.Field(description="Name of the company")
company_website: str = pydantic.Field(description="Company website URL")
class CompanyOutput(pydantic.BaseModel):
key_insights: list[str] = pydantic.Field(description="Key business insights")
market_position: str = pydantic.Field(description="Market positioning analysis")
# Create reusable task specification
task_spec = TaskSpecParam(
input_schema=JsonSchemaParam(json_schema=CompanyInput.model_json_schema()),
output_schema=JsonSchemaParam(json_schema=CompanyOutput.model_json_schema()),
)
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
// Define your input and output types
interface CompanyInput {
company_name: string;
company_website: string;
}
interface CompanyOutput {
key_insights: string[];
market_position: string;
}
// Use SDK types for Task Group API
type TaskGroupObject = Parallel.TaskGroup;
type TaskGroupStatus = Parallel.TaskGroupStatus;
type TaskGroupRunResponse = Parallel.TaskGroupRunResponse;
type TaskGroupEventsResponse = Parallel.TaskGroupEventsResponse;
type TaskGroupGetRunsResponse = Parallel.TaskGroupGetRunsResponse;
// Create reusable task specification using SDK types
const taskSpec: Parallel.TaskSpec = {
input_schema: {
type: "json",
json_schema: {
type: "object",
properties: {
company_name: {
type: "string",
description: "Name of the company",
},
company_website: {
type: "string",
description: "Company website URL",
},
},
required: ["company_name", "company_website"],
},
},
output_schema: {
type: "json",
json_schema: {
type: "object",
properties: {
key_insights: {
type: "array",
items: { type: "string" },
description: "Key business insights",
},
market_position: {
type: "string",
description: "Market positioning analysis",
},
},
required: ["key_insights", "market_position"],
},
},
};
```
### 2. Create a Task Group
```bash cURL theme={"system"}
# Create task group and capture the ID
response=$(curl --request POST \
--url https://api.parallel.ai/v1/tasks/groups \
--header 'Content-Type: application/json' \
--header "x-api-key: ${PARALLEL_API_KEY}" \
--data '{}')
# Extract taskgroup_id from response
TASKGROUP_ID=$(echo $response | jq -r '.taskgroup_id')
echo "Created task group: $TASKGROUP_ID"
```
```python Python theme={"system"}
# Initialize the client
client = AsyncParallel()
# Create a new task group
task_group = await client.task_group.create()
taskgroup_id = task_group.task_group_id
print(f"Created task group: {taskgroup_id}")
```
```typescript TypeScript theme={"system"}
// Initialize the client
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
// Create a new task group
const groupResponse = await client.taskGroup.create({});
const taskgroupId = groupResponse.taskgroup_id;
console.log(`Created task group: ${taskgroupId}`);
```
### 3. Add Tasks to the Group
By default, the response refreshes and returns the aggregated status of all runs in the group. If you're adding tasks at scale, set `refresh_status` to `false` to skip the full status refresh — the response still includes a status snapshot from the group's atomically maintained counters. You can refresh the aggregated status at any time via the [GET task-group endpoint](/api-reference/tasks/retrieve-task-group).
```bash cURL theme={"system"}
curl --request POST \
--url https://api.parallel.ai/v1/tasks/groups/${TASKGROUP_ID}/runs \
--header 'Content-Type: application/json' \
--header "x-api-key: ${PARALLEL_API_KEY}" \
--data '{
"default_task_spec": '$TASK_SPEC',
"inputs": [
{
"input": {
"company_name": "Acme Corp",
"company_website": "https://acme.com"
},
"processor": "pro"
},
{
"input": {
"company_name": "TechStart",
"company_website": "https://techstart.io"
},
"processor": "pro"
}
]
}'
```
```python Python theme={"system"}
# Prepare your inputs
companies = [
{"company_name": "Acme Corp", "company_website": "https://acme.com"},
{"company_name": "TechStart", "company_website": "https://techstart.io"},
# ... more companies
]
# Create task run inputs
run_inputs = [
RunInputParam(
input=CompanyInput(**company).model_dump(),
processor="pro",
)
for company in companies
]
# Add runs to the group
response = await client.task_group.add_runs(
taskgroup_id,
inputs=run_inputs,
default_task_spec=task_spec,
)
print(f"Added {len(response.run_ids)} Tasks to group")
```
```typescript TypeScript theme={"system"}
// Prepare your inputs
const companies = [
{ company_name: "Acme Corp", company_website: "https://acme.com" },
{ company_name: "TechStart", company_website: "https://techstart.io" },
// ... more companies
];
// Create task run inputs using SDK types
const runInputs: Array = companies.map((company) => ({
input: {
company_name: company.company_name,
company_website: company.company_website,
},
processor: "pro",
}));
// Add runs to the group
const response = await client.taskGroup.addRuns(taskgroupId, {
default_task_spec: taskSpec,
inputs: runInputs,
});
console.log(`Added ${response.run_ids.length} Tasks to group`);
```
### 4. Monitor Progress
```bash cURL theme={"system"}
# Get status of the group
curl --request GET \
--url https://api.parallel.ai/v1/tasks/groups/${TASKGROUP_ID} \
--header "x-api-key: ${PARALLEL_API_KEY}"
# Get status of all runs in the group
curl --request GET \
--no-buffer \
--url https://api.parallel.ai/v1/tasks/groups/${TASKGROUP_ID}/runs \
--header "x-api-key: ${PARALLEL_API_KEY}"
```
```python Python theme={"system"}
import asyncio
async def wait_until_inactive(client: AsyncParallel, taskgroup_id: str) -> None:
while True:
task_group = await client.task_group.retrieve(taskgroup_id)
status = task_group.status
print(f"Status: {status.task_run_status_counts}")
if not status.is_active:
print("No active runs remain.")
break
await asyncio.sleep(10)
asyncio.run(wait_until_inactive(client, taskgroup_id))
```
```typescript TypeScript theme={"system"}
async function waitUntilInactive(
client: Parallel,
taskgroupId: string
): Promise {
while (true) {
const response = await client.taskGroup.retrieve(taskgroupId);
const status = response.status;
console.log("Status:", status.task_run_status_counts);
if (!status.is_active) {
console.log("No active runs remain.");
break;
}
await new Promise((resolve) => setTimeout(resolve, 10000));
}
}
async function main() {
const client = new Parallel({ apiKey: process.env["PARALLEL_API_KEY"] });
// ... create task group and get taskgroupId ...
await waitUntilInactive(client, taskgroupId);
}
main();
```
### 5. Retrieve Results
The `getRuns` endpoint returns a **Server-Sent Events stream**, not a simple JSON response. It emits one event per run currently in the group (a snapshot of each run's state), then closes. To pick up runs added after that snapshot, resume from the last `event_id` via the `last_event_id` parameter.
Each event in the stream has:
* `type`: Either `"task_run.state"` or `"error"`
* `event_id`: Cursor for resuming the stream via the `last_event_id` parameter
* `run`: The `TaskRun` object with `run_id`, `status`, and `is_active`
* `input`: The original input (only included when `include_input=true`)
* `output`: The result output (only included when `include_output=true` **and** the run completed successfully)
If you want a live stream of non-active run state transitions and group-level status updates instead of a snapshot, use the `/events` endpoint shown below the `getRuns` examples.
```bash cURL theme={"system"}
# Snapshot of each run's current state (matches the Python/TS getRuns examples)
curl --request GET \
--no-buffer \
--url "https://api.parallel.ai/v1/tasks/groups/${TASKGROUP_ID}/runs?include_input=true&include_output=true" \
--header "x-api-key: ${PARALLEL_API_KEY}"
# Live stream of non-active run-state + group-status events (stays open while runs are active)
curl --request GET \
--no-buffer \
--url https://api.parallel.ai/v1/tasks/groups/${TASKGROUP_ID}/events \
--header "x-api-key: ${PARALLEL_API_KEY}"
```
```python Python theme={"system"}
from parallel.types.task_run_event import TaskRunEvent
from parallel.types.error_event import ErrorEvent
# Stream all results from the group
async def get_all_results(client: AsyncParallel, taskgroup_id: str):
results = []
run_stream = await client.task_group.get_runs(
taskgroup_id,
include_input=True,
include_output=True,
)
async for event in run_stream:
if isinstance(event, TaskRunEvent) and event.output and event.input:
company_input = CompanyInput.model_validate(event.input.input)
company_output = CompanyOutput.model_validate(event.output.content)
results.append(
{
"company": company_input.company_name,
"insights": company_output.key_insights,
"market_position": company_output.market_position,
}
)
elif isinstance(event, ErrorEvent):
print(f"Error: {event.error}")
return results
results = await get_all_results(client, taskgroup_id)
print(f"Processed {len(results)} companies successfully")
```
```typescript TypeScript theme={"system"}
// Stream all results from the group
async function getAllResults(
client: Parallel,
taskgroupId: string
): Promise<
Array<{ company: string; insights: string[]; market_position: string }>
> {
const results: Array<{
company: string;
insights: string[];
market_position: string;
}> = [];
// Use the SDK's streaming API
const runStream = await client.taskGroup.getRuns(taskgroupId, {
include_input: true,
include_output: true,
});
for await (const event of runStream) {
// Handle task run events
if (event.type === "task_run.state" && event.output && event.input) {
const input = event.input.input as unknown as CompanyInput;
const output = (event.output as Parallel.TaskRunJsonOutput)
.content as unknown as CompanyOutput;
results.push({
company: input.company_name,
insights: output.key_insights,
market_position: output.market_position,
});
}
}
return results;
}
const results = await getAllResults(client, taskgroupId);
console.log(`Processed ${results.length} companies successfully`);
```
## Batch Processing Pattern
For large datasets, process Tasks in batches to optimize performance. Setting `refresh_status` to `false` is recommended when adding tasks in bulk, as it skips refreshing the group status on each request for faster responses:
```python Python theme={"system"}
async def process_companies_in_batches(
client: AsyncParallel,
taskgroup_id: str,
companies: list[dict[str, str]],
batch_size: int = 500,
) -> None:
total_created = 0
for i in range(0, len(companies), batch_size):
batch = companies[i : i + batch_size]
# Create run inputs for this batch
run_inputs = [
RunInputParam(
input=CompanyInput(**company).model_dump(),
processor="pro",
)
for company in batch
]
# Add batch to group (skip status refresh for faster bulk adds)
response = await client.task_group.add_runs(
taskgroup_id,
inputs=run_inputs,
default_task_spec=task_spec,
refresh_status=False,
)
total_created += len(response.run_ids)
print(f"Processed {i + len(batch)} companies. Created {total_created} Tasks.")
```
```typescript TypeScript theme={"system"}
async function processCompaniesInBatches(
client: Parallel,
taskgroupId: string,
companies: Array<{ company_name: string; company_website: string }>,
batchSize: number = 500
): Promise {
let totalCreated = 0;
for (let i = 0; i < companies.length; i += batchSize) {
const batch = companies.slice(i, i + batchSize);
// Create run inputs for this batch using SDK types
const runInputs: Array = batch.map((company) => ({
input: {
company_name: company.company_name,
company_website: company.company_website,
},
processor: "pro",
}));
// Add batch to group (skip status refresh for faster bulk adds)
const response = await client.taskGroup.addRuns(taskgroupId, {
default_task_spec: taskSpec,
inputs: runInputs,
refresh_status: false,
});
totalCreated += response.run_ids.length;
console.log(
`Processed ${i + batch.length} companies. Created ${totalCreated} Tasks.`
);
}
}
```
## Error Handling
The `/runs` stream is a snapshot and can include active runs. Classify each event using `event.run.status`, not merely by whether an output is present:
```python Python theme={"system"}
async def process_with_error_handling(client: AsyncParallel, taskgroup_id: str):
successful_results = []
unsuccessful_results = []
unresolved_results = []
run_stream = await client.task_group.get_runs(
taskgroup_id,
include_input=True,
include_output=True,
)
async for event in run_stream:
if isinstance(event, ErrorEvent):
unsuccessful_results.append(event)
continue
if not isinstance(event, TaskRunEvent):
continue
if event.run.status == "completed" and event.output:
try:
# Validate the result
company_output = CompanyOutput.model_validate(event.output.content)
successful_results.append(event)
except Exception as e:
print(f"Validation error: {e}")
unsuccessful_results.append(event)
elif event.run.status in {"failed", "cancelled", "completed"}:
# A completed run without output is also unsuccessful.
unsuccessful_results.append(event)
else:
# queued, running, cancelling, or action_required
unresolved_results.append(event)
print(
f"Success: {len(successful_results)}, "
f"Unsuccessful: {len(unsuccessful_results)}, "
f"Unresolved: {len(unresolved_results)}"
)
return successful_results, unsuccessful_results, unresolved_results
```
```typescript TypeScript theme={"system"}
async function processWithErrorHandling(
client: Parallel,
taskgroupId: string
): Promise<{
successful: Array;
unsuccessful: Array;
unresolved: Array;
}> {
const successful: Array = [];
const unsuccessful: Array = [];
const unresolved: Array = [];
const runStream = await client.taskGroup.getRuns(taskgroupId, {
include_input: true,
include_output: true,
});
for await (const event of runStream) {
if (event.type === "error") {
unsuccessful.push(event);
continue;
}
if (event.run.status === "completed" && event.output) {
successful.push(event);
} else if (
event.run.status === "failed" ||
event.run.status === "cancelled" ||
event.run.status === "completed"
) {
// A completed run without output is also unsuccessful.
unsuccessful.push(event);
} else {
// queued, running, cancelling, or action_required
unresolved.push(event);
}
}
console.log(
`Success: ${successful.length}, Unsuccessful: ${unsuccessful.length}, Unresolved: ${unresolved.length}`
);
return { successful, unsuccessful, unresolved };
}
```
## Complete Example
Here's a complete script that demonstrates the full workflow, including all of
the setup code above.
```python Python [expandable] theme={"system"}
import asyncio
import pydantic
from parallel import AsyncParallel
from parallel.types import JsonSchemaParam, TaskGroupStatus, TaskSpecParam
from parallel.types.run_input_param import RunInputParam
from parallel.types.task_run_event import TaskRunEvent
from parallel.types.error_event import ErrorEvent
# Define your input and output models
class CompanyInput(pydantic.BaseModel):
company_name: str = pydantic.Field(description="Name of the company")
company_website: str = pydantic.Field(description="Company website URL")
class CompanyOutput(pydantic.BaseModel):
key_insights: list[str] = pydantic.Field(description="Key business insights")
market_position: str = pydantic.Field(description="Market positioning analysis")
# Create reusable task specification
task_spec = TaskSpecParam(
input_schema=JsonSchemaParam(json_schema=CompanyInput.model_json_schema()),
output_schema=JsonSchemaParam(json_schema=CompanyOutput.model_json_schema()),
)
async def wait_until_inactive(
client: AsyncParallel, taskgroup_id: str
) -> TaskGroupStatus:
while True:
task_group = await client.task_group.retrieve(taskgroup_id)
status = task_group.status
print(f"Status: {status.task_run_status_counts}")
if not status.is_active:
print("No active runs remain.")
return status
await asyncio.sleep(10)
async def get_all_results(client: AsyncParallel, taskgroup_id: str):
results = []
run_stream = await client.task_group.get_runs(
taskgroup_id,
include_input=True,
include_output=True,
)
async for event in run_stream:
if isinstance(event, TaskRunEvent) and event.output and event.input:
company_input = CompanyInput.model_validate(event.input.input)
company_output = CompanyOutput.model_validate(event.output.content)
results.append(
{
"company": company_input.company_name,
"insights": company_output.key_insights,
"market_position": company_output.market_position,
}
)
elif isinstance(event, ErrorEvent):
print(f"Error: {event.error}")
return results
async def batch_company_research():
client = AsyncParallel()
# Create task group
task_group = await client.task_group.create()
taskgroup_id = task_group.task_group_id
print(f"Created taskgroup id {taskgroup_id}")
# Define companies to research
companies = [
{"company_name": "Stripe", "company_website": "https://stripe.com"},
{"company_name": "Shopify", "company_website": "https://shopify.com"},
{"company_name": "Salesforce", "company_website": "https://salesforce.com"},
]
# Add Tasks to group
run_inputs = [
RunInputParam(
input=CompanyInput(**company).model_dump(),
processor="pro",
)
for company in companies
]
response = await client.task_group.add_runs(
taskgroup_id,
inputs=run_inputs,
default_task_spec=task_spec,
)
print(f"Added {len(response.run_ids)} runs to taskgroup {taskgroup_id}")
# Wait until no runs are active and get results
final_status = await wait_until_inactive(client, taskgroup_id)
results = await get_all_results(client, taskgroup_id)
if len(results) == len(companies):
print(f"Successfully processed all {len(results)} companies")
else:
print(f"Successfully processed {len(results)} of {len(companies)} companies")
print(f"Final run status counts: {final_status.task_run_status_counts}")
return results
# Run the batch job
results = asyncio.run(batch_company_research())
```
```typescript TypeScript [expandable] theme={"system"}
import Parallel from "parallel-web";
// Define your input and output types
interface CompanyInput {
company_name: string;
company_website: string;
}
interface CompanyOutput {
key_insights: string[];
market_position: string;
}
// Use SDK types for Task Group API
type TaskGroupObject = Parallel.TaskGroup;
type TaskGroupGetRunsResponse = Parallel.TaskGroupGetRunsResponse;
// Create reusable task specification using SDK types
const taskSpec: Parallel.TaskSpec = {
input_schema: {
type: "json",
json_schema: {
type: "object",
properties: {
company_name: {
type: "string",
description: "Name of the company",
},
company_website: {
type: "string",
description: "Company website URL",
},
},
required: ["company_name", "company_website"],
},
},
output_schema: {
type: "json",
json_schema: {
type: "object",
properties: {
key_insights: {
type: "array",
items: { type: "string" },
description: "Key business insights",
},
market_position: {
type: "string",
description: "Market positioning analysis",
},
},
required: ["key_insights", "market_position"],
},
},
};
async function waitUntilInactive(
client: Parallel,
taskgroupId: string
): Promise {
while (true) {
const response = await client.taskGroup.retrieve(taskgroupId);
const status = response.status;
console.log("Status:", status.task_run_status_counts);
if (!status.is_active) {
console.log("No active runs remain.");
return status;
}
await new Promise((resolve) => setTimeout(resolve, 10000));
}
}
async function getAllResults(
client: Parallel,
taskgroupId: string
): Promise<
Array<{ company: string; insights: string[]; market_position: string }>
> {
const results: Array<{
company: string;
insights: string[];
market_position: string;
}> = [];
const runStream = await client.taskGroup.getRuns(taskgroupId, {
include_input: true,
include_output: true,
});
for await (const event of runStream) {
if (event.type === "task_run.state" && event.output && event.input) {
const input = event.input.input as unknown as CompanyInput;
const output = (event.output as Parallel.TaskRunJsonOutput)
.content as unknown as CompanyOutput;
results.push({
company: input.company_name,
insights: output.key_insights,
market_position: output.market_position,
});
}
}
return results;
}
async function batchCompanyResearch(): Promise<
Array<{ company: string; insights: string[]; market_position: string }>
> {
const client = new Parallel({
apiKey: process.env.PARALLEL_API_KEY,
});
// Create task group
const groupResponse = await client.taskGroup.create({});
const taskgroupId = groupResponse.taskgroup_id;
console.log(`Created taskgroup id ${taskgroupId}`);
// Define companies to research
const companies = [
{ company_name: "Stripe", company_website: "https://stripe.com" },
{ company_name: "Shopify", company_website: "https://shopify.com" },
{ company_name: "Salesforce", company_website: "https://salesforce.com" },
];
// Add Tasks to group
const runInputs: Array = companies.map((company) => ({
input: {
company_name: company.company_name,
company_website: company.company_website,
},
processor: "pro",
}));
const response = await client.taskGroup.addRuns(taskgroupId, {
default_task_spec: taskSpec,
inputs: runInputs,
});
console.log(
`Added ${response.run_ids.length} runs to taskgroup ${taskgroupId}`
);
// Wait until no runs are active and get results
const finalStatus = await waitUntilInactive(client, taskgroupId);
const results = await getAllResults(client, taskgroupId);
if (results.length === companies.length) {
console.log(`Successfully processed all ${results.length} companies`);
} else {
console.log(
`Successfully processed ${results.length} of ${companies.length} companies`
);
console.log("Final run status counts:", finalStatus.task_run_status_counts);
}
return results;
}
// Run the batch job
const results = await batchCompanyResearch();
```
# Research Basis
Source: https://docs.parallel.ai/task-api/guides/access-research-basis
Understand how to access citations, reasoning, and confidence levels for your Task Run outputs
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
When you execute a task using the Task API, the response includes both the generated output and its corresponding research basis, a structured explanation detailing the reasoning and evidence behind each result. This transparency enables you to understand how the system arrived at its conclusions and to assess the reliability of the output.
## Terminology
To avoid confusion, this document uses the following terminology:
* **Research Basis**: The overall feature that provides transparency into how Task API results are generated
* **Basis**: The `basis` field in the API response, which contains an array of field-specific evidence
* **FieldBasis**: The specific object type that contains citations, reasoning, and confidence for individual output fields
## Task Run Result
Every Task Run Result object contains the following fields:
| Field | Type | Description |
| - | - | - |
| `run` | object | Task Run object with status and id, detailed above. |
| `output` | TaskRunOutput object or null | Output from the task conforming to the output schema. Present iff run.status == completed. |
A TaskRunOutput object can be one of two types:
* TaskRunTextOutput
* TaskRunJsonOutput
Both have the following fields:
| Field | Type | Description |
| - | - | - |
| `content` | object for TaskRunJsonOutput; string for TaskRunTextOutput | JSON or plain text according to the output schema. |
| `basis` | array\[FieldBasis] | Array of FieldBasis objects. JSON outputs include entries for top-level fields and individual elements of top-level list fields. Text outputs include a single entry for `output`. See the FieldBasis object below. |
| `type` | string | `json` for TaskRunJsonOutput or `text` for TaskRunTextOutput. |
## Research Basis Structure
The `basis` field contains FieldBasis objects. For JSON outputs, it includes an entry for each top-level field and for each individual element of a top-level list field. For list elements, the `field` value uses a dot-delimited index, such as `key_executives.0` and `key_executives.1`. The parent entry remains available alongside the element entries. Text outputs include a single entry whose `field` value is `output`. These entries let you trace which web sources contributed to each piece of information in your result.
```mermaid theme={"system"}
graph TD
TaskRunResult[Task Run Result]
TaskRunOutput[Task Run Output]
Content[Output Content]
Basis[Output Basis]
TaskRunResult --> TaskRunOutput
TaskRunOutput --> Content
TaskRunOutput --> Basis
Content --> Field1["Company: Microsoft"]
Content --> Field2["Founded: 1975"]
Basis --> BasisField1[Basis for Company]
Basis --> BasisField2[Basis for Founded]
BasisField1 --> C1[Citations + Reasoning]
BasisField2 --> C2[Citations + Reasoning]
style TaskRunResult fill:#FCFCFA,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style TaskRunOutput fill:#FCFCFA,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style Content fill:#FCFCFA,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style Field1 fill:#FCFCFA,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style Field2 fill:#FCFCFA,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style Basis fill:#D8D0BF,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style BasisField1 fill:#D8D0BF,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style BasisField2 fill:#D8D0BF,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style C1 fill:#FB631B,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
style C2 fill:#FB631B,stroke:#1D1B16,stroke-width:1px,color:#1D1B16
```
## The FieldBasis object
Each FieldBasis object has these components:
| Field | Type | Description |
| - | - | - |
| `field` | string | Top-level JSON output field name, dot-delimited list-element path, or `output` for text output. |
| `citations` | array\[Citation] | List of web sources supporting the output field |
| `reasoning` | string | Explanation of how the system processed the information |
| `confidence` | string or null | Reliability rating for each output field |
### Citations
Citations provide the exact URLs where information was found. Each citation includes excerpts from the source that contributed to the output:
| Field | Type | Description |
| - | - | - |
| `url` | string | The source URL |
| `excerpts` | array\[string] or null | Relevant text from the source |
Having multiple citations for an output field often indicates stronger evidence, as the information was verified across multiple sources.
### Reasoning
The reasoning field explains how the system evaluated, compared, and synthesized information from different sources. This is particularly valuable when:
* Information from different sources needed to be reconciled
* Calculations or conversions were performed
* The system needed to make judgments about conflicting data
### Confidence Levels
All processors include a confidence rating for each output field:
* **High**: Strong evidence from multiple authoritative sources with consistent information
* **Medium**: Adequate evidence but with some inconsistencies or from less authoritative sources
* **Low**: Limited or conflicting evidence, or information from less reliable sources
### Per-Element Basis
For JSON outputs, the Task API returns a `basis` entry for each top-level output field. For a top-level list field, it also returns entries for individual list elements. Element entries use dot-delimited field paths such as `key_executives.0` and `key_executives.1`.
Per-element entries supplement the parent field entry. They do not replace it. For example, a response can contain `key_executives`, `key_executives.0`, and `key_executives.1`.
As of August 24, 2026, omit the legacy `parallel-beta: field-basis-2025-11-25` header from new integrations, and remove it from existing integrations. You receive Per-Element Basis without it.
If you did not send the beta header before August 24, 2026, your `basis` arrays may contain more entries. Continue reading task result values from `output.content`.
## Examples
### Output with Research Basis
Here's an example of a complete Task Run output that includes research basis information:
```json theme={"system"}
{
"content": {
"company": "Microsoft",
"founded": "1975",
"headquarters": "Redmond, Washington, USA"
},
"basis": [
{
"field": "company",
"citations": [
{
"url": "https://www.microsoft.com/en-us/about",
"excerpts": ["Microsoft Corporation is an American multinational technology corporation headquartered in Redmond, Washington."]
}
],
"reasoning": "The company name is clearly identified on the official corporate website.",
"confidence": "high"
},
{
"field": "founded",
"citations": [
{
"url": "https://www.microsoft.com/en-us/about/company",
"excerpts": ["Founded in 1975, Microsoft (Nasdaq \"MSFT\") enables digital transformation for the era of an intelligent cloud and an intelligent edge."]
},
{
"url": "https://en.wikipedia.org/wiki/Microsoft",
"excerpts": ["Microsoft Corporation was founded by Bill Gates and Paul Allen on April 4, 1975, to develop and sell BASIC interpreters for the Altair 8800."]
}
],
"reasoning": "Multiple authoritative sources consistently state 1975 as the founding year. The official company website and Wikipedia both confirm this date, with Wikipedia providing the specific day (April 4).",
"confidence": "high"
},
{
"field": "headquarters",
"citations": [
{
"url": "https://www.microsoft.com/en-us/about/company",
"excerpts": ["Headquartered in Redmond, Washington, Microsoft has offices in over 100 countries."]
}
],
"reasoning": "The official company website explicitly states the headquarters location as Redmond, Washington, USA.",
"confidence": "high"
}
],
"type": "json"
}
```
### High vs. Low Confidence Outputs
```json theme={"system"}
{
"field": "revenue",
"citations": [
{
"url": "https://www.microsoft.com/en-us/Investor/earnings/FY-2023-Q4/press-release-webcast",
"excerpts": ["Microsoft reported fiscal year 2023 revenue of $211.9 billion, an increase of 7% compared to the previous fiscal year."]
},
{
"url": "https://www.sec.gov/Archives/edgar/data/789019/000095017023014837/msft-20230630.htm",
"excerpts": ["Revenue was $211.9 billion for fiscal year 2023, up 7% compared to $198.3 billion for fiscal year 2022."]
}
],
"reasoning": "The revenue figure is consistent across both the company's investor relations page and their official SEC filing. Both sources explicitly state the fiscal year 2023 revenue as $211.9 billion, representing a 7% increase over the previous year.",
"confidence": "high"
}
```
```json theme={"system"}
{
"field": "crm_system",
"citations": [
{
"url": "https://www.linkedin.com/jobs/view/sales-representative-microsoft-dynamics-365-at-contoso-inc-3584271",
"excerpts": ["Looking for sales professionals with experience in Microsoft Dynamics 365 CRM to join our growing team."]
}
],
"reasoning": "There is limited direct evidence about which CRM system the company uses internally. The job posting suggests they work with Microsoft Dynamics 365, but it's not explicitly stated whether this is their primary internal CRM or simply a product they sell/support. No official company documentation confirming their internal CRM system was found.",
"confidence": "low"
}
```
### Per-Element Basis example
When an output contains a list, the research basis includes a parent entry for the list and separate entries for its elements. Use the parent entry for field-level context and element paths for item-level evidence.
```json theme={"system"}
{
"content": {
"company": "OpenAI",
"key_executives": [
{"name": "Sam Altman", "title": "CEO"},
{"name": "Greg Brockman", "title": "President"}
]
},
"basis": [
{
"field": "company",
"citations": [
{
"url": "https://openai.com/about",
"excerpts": ["OpenAI is headquartered in San Francisco, California."]
}
],
"reasoning": "The company name is taken from the official about page.",
"confidence": "high"
},
{
"field": "key_executives",
"citations": [
{
"url": "https://openai.com/leadership",
"excerpts": ["Key executives include Sam Altman and Greg Brockman."]
}
],
"reasoning": "The leadership page lists each executive explicitly.",
"confidence": "high"
},
{
"field": "key_executives.0",
"citations": [
{
"url": "https://openai.com/leadership",
"excerpts": ["Sam Altman serves as the CEO of OpenAI."]
}
],
"reasoning": "Same source, filtered down to the first list element.",
"confidence": "high"
},
{
"field": "key_executives.1",
"citations": [
{
"url": "https://openai.com/leadership",
"excerpts": ["Greg Brockman serves as President."]
}
],
"reasoning": "Same source, filtered down to the second list element.",
"confidence": "high"
}
],
"type": "json"
}
```
# Processors
Source: https://docs.parallel.ai/task-api/guides/choose-a-processor
Select the right Task API processor (lite, base, core, pro, ultra) based on task complexity and latency requirements
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
Processors are the engines that execute Task Runs. The choice of Processor determines the performance profile and reasoning behavior used. Any Task Run can be executed on any Processor.
Choose a processor based on **task complexity**. Use `lite` or `base` for simple enrichments, `core` for reliable accuracy on up to 10 output fields, and `pro` or `ultra` when reasoning depth is critical. For latency-sensitive workflows—including agentic research where an agent is waiting on results—use the [Responses API](/responses-api/responses-quickstart) instead.
Each processor varies in performance characteristics and supported features. Use the table below to compare processors.
**About "Max Fields" (the `~` symbol):** The max fields column shows approximate limits because actual capacity depends on field complexity. Simple fields like dates or booleans use less capacity than complex fields requiring extensive research. A task with 5 complex analytical fields may require more processing than one with 15 simple lookup fields. Use these numbers as guidelines. If you're near the limit and seeing quality issues, try a higher-tier processor.
| Processor | Latency | Strengths | Max Fields |
| - | - | - | - |
| `lite` | 10s - 2min | Basic metadata, fallback, low latency | \~2 fields |
| `base` | 15s - 3min | Reliable standard enrichments | \~5 fields |
| `core` | 60s - 5min | Cross-referenced, moderately complex outputs | \~10 fields |
| `core2x` | 60s - 10min | High complexity cross referenced outputs | \~10 fields |
| `pro` | 2min - 10min | Exploratory web research | \~20 fields |
| `ultra` | 3min - 25min | Advanced multi-source deep research | \~20 fields |
| `ultra2x` | 5min - 50min | Difficult deep research | \~25 fields |
| `ultra4x` | 5min - 90min | Very difficult deep research | \~25 fields |
| `ultra8x` | 5min - 2hr | The most difficult deep research | \~25 fields |
See [Pricing](/getting-started/pricing) for processor costs and all API rates.
## Observed Latency
The table below shows median (p50) and 90th percentile (p90) **execution times** measured in production for standard processors across all customer workloads. Your results will vary with task complexity, output schema size, and input difficulty.
| Processor | p50 | p90 |
| - | - | - |
| `lite` | 45s | 1.5min |
| `base` | 50s | 2min |
| `core` | 1.5min | 3min |
| `core2x` | 3.5min | 6.5min |
| `pro` | 3.5min | 7.5min |
| `ultra` | 4min | 10min |
| `ultra2x` | 6min | 9.5min |
| `ultra4x` | 8min | 10.5min |
| `ultra8x` | 6.5min | 11min |
### Execution Time vs Queue Time
A Task Run moves through three states: **queued → running → completed** on success, or transitions to **failed** from either **queued** or **running**. The latencies above measure only the `running` phase—the time a processor actively spends executing your task. Time spent in the queue **is not included**.
Queue time varies with your workload. Runs execute concurrently, but processing capacity is finite: when a large burst of runs is submitted at once, runs beyond the available capacity wait in the queue until capacity frees up. End-to-end time (creation to completion) can therefore exceed the execution time ranges above.
For latency-sensitive workflows, use the [Responses API](/responses-api/responses-quickstart).
## Latency-Sensitive Workflows
The Task API is designed for asynchronous, accuracy-critical research: runs are queued, executed, and retrieved when complete. If your use case is latency-sensitive—an interactive application, a subagent call, or any agentic research workflow where a caller is actively waiting on results—use the [Responses API](/responses-api/responses-quickstart) instead. It is purpose-built for low-latency requests.
## Examples
Processors can be used flexibly depending on the scope and structure of your task. The examples below show how to:
* Use a single processor (like `lite`, `base`, `core`, `pro`, or `ultra`) to handle specific types of input and reasoning depth.
* Chain processors together to combine fast lookups with deeper synthesis.
This structure enables flexibility across a variety of tasks—whether you're extracting metadata, enriching structured records, or generating analytical reports.
### Sample Task for each Processor
```python lite theme={"system"}
task_run = client.task_run.create(
input="Parallel Web Systems (parallel.ai)",
task_spec={"output_schema":"The founding date of the company in the format MM-YYYY"},
processor="lite"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
```python base theme={"system"}
task_run = client.task_run.create(
input="Parallel Web Systems (parallel.ai)",
task_spec={"output_schema":"The founding date and most recent product launch of the company"},
processor="base"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
```python core theme={"system"}
task_run = client.task_run.create(
input="Parallel Web Systems (parallel.ai)",
task_spec={"output_schema":"The founding date, founders, and most recent product launch of the company"},
processor="core"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
```python pro theme={"system"}
task_run = client.task_run.create(
input="Parallel Web Systems (parallel.ai)",
task_spec={"output_schema":"The founding date, founders, mission, benchmarked competitors and most recent product launch of the company"},
processor="pro"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
```python ultra theme={"system"}
task_run = client.task_run.create(
input="Parallel Web Systems (parallel.ai)",
task_spec={"output_schema":"A comprehensive analysis of the industry of the company, including growth factors and major competitors."},
processor="ultra"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
### Multi-Processor Workflows
You can combine processors in sequence to support more advanced workflows.
Start by retrieving basic information with `base`:
```python theme={"system"}
task_run_base = client.task_run.create(
input="Pfizer",
task_spec={"output_schema":"Who are the current executive leaders at Pfizer? Include their full name and title. Ensure that you retrieve this information from a reliable source, such as major news outlets or the company website."},
processor="base"
)
print(f"Run ID: {task_run_base.run_id}")
base_result = client.task_run.result(task_run_base.run_id, api_timeout=3600)
print(base_result.output)
```
Then use the result as input to `core` to generate detailed background information:
```python theme={"system"}
import json
task_run = client.task_run.create(
input=json.dumps(base_result.output.content),
task_spec={"output_schema":"For the executive provided, find their professional background tenure at their current company, and notable strategic responsibilities."},
processor="pro"
)
print(f"Run ID: {task_run.run_id}")
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
This lets you use a lower compute processor for initial retrieval, then switch to a more capable one for analysis and context-building.
# Task Runs Lifecycle
Source: https://docs.parallel.ai/task-api/guides/execute-task-run
Understanding how Tasks Runs are created, processed, and returned
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
Task runs are stateful objects, which means that creating a Task Run and retrieving its results are separate calls to the API. Each Task Run is an independent instance that progresses through a series of states from `queued` to `completed`. This asynchronous design enables efficient scaling of web research operations.
### Task Run States
| Status | Description | Can Transition To |
| - | - | - |
| `queued` | Task created and waiting for processing | `running`, `failed` |
| `running` | Task actively being processed | `completed`, `failed` |
| `completed` | Task successfully completed with results available | (Terminal state) |
| `failed` | Task encountered an error | (Terminal state) |
**Note**: Running time varies by processor type and task complexity.
## Creating a Task Run
The basic requirements of a task run are:
* Input data (string or JSON object)
* A processor selection
* Output schema (optional but recommended)
Optionally, you can include:
* Input schema (optionally used to validate run input)
* Metadata (for tracking or organizing runs)
A Task Run, once created, can be identified by its `run_id`. A Task Run Result can be accessed once the Task Run status becomes `completed`.
## Rate Limits
The Task API enforces a limit of **2,000 requests per minute** per API key. This limit applies across all POST and GET requests and helps ensure consistent performance for all users.
When you exceed this limit, the API returns a `429 Too Many Requests` status code.
## Examples
### Completed Status Example
```json theme={"system"}
{
"run_id": "b0679f70-195e-4f42-8b8a-b8242a0c69c7",
"status": "completed",
"is_active": false,
"result": {
"population": "29.5 million (2022 estimate)",
"growth_rate": "1.4% annually",
"major_cities": [
"Houston",
"Dallas",
"San Antonio"
]
},
"result_url": "https://api.parallel.ai/v1/tasks/runs/b0679f70-195e-4f42-8b8a-b8242a0c69c7/result",
"warnings": null,
"errors": null,
"processor": "core",
"metadata": null,
"created_at": "2025-04-08T04:28:59.913464",
"modified_at": "2025-04-08T04:29:32.651298"
}
```
### Failed Status Example
If a Task Run encounters an error, the status will be set to `failed` and details will be available in the `errors` field:
```json theme={"system"}
{
"run_id": "b0679f70-195e-4f42-8b8a-b8242a0c69c7",
"status": "failed",
"is_active": false,
"result": null,
"result_url": null,
"warnings": null,
"errors": [
{
"code": "processing_error",
"message": "Unable to process task due to invalid input format",
"details": {
"field": "input",
"reason": "Expected JSON object but received string"
}
}
],
"processor": "core",
"metadata": null,
"created_at": "2025-04-08T04:28:59.913464",
"modified_at": "2025-04-08T04:29:01.234567"
}
```
# Interactions
Source: https://docs.parallel.ai/task-api/guides/interactions
Chain context across multiple API calls using interaction IDs
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## Overview
Interactions let you chain context between API calls. Each response includes an `interaction_id`, which you can pass as `previous_interaction_id` in a subsequent request to carry forward the context from the previous call. This enables multi-turn research workflows like follow-up questions, iterative refinement, and conversational agents.
Interactions are available on the [Task API](/task-api/task-quickstart).
## How It Works
1. **Make an initial request** — the response includes an `interaction_id`
2. **Pass it forward** — include that value as `previous_interaction_id` in your next request
3. **Continue the chain** — each new response returns its own `interaction_id`, so you can keep building on prior context
## Request and Response Fields
| Field | Location | Description |
| - | - | - |
| `previous_interaction_id` | Request (optional) | Interaction ID from a prior call to use as context |
| `interaction_id` | Response | Identifier for this interaction — pass as `previous_interaction_id` in follow-ups |
These fields are available on Task API (`BetaTaskRunInput`) requests and their corresponding responses.
## Task API Usage
```python Python theme={"system"}
from parallel import Parallel
from parallel.types import TaskSpecParam, TextSchemaParam
client = Parallel()
# Initial request
task_run = client.task_run.create(
input="What are the main pricing models used by SaaS companies?",
processor="core",
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
)
result = client.task_run.result(task_run.run_id, api_timeout=3600)
# Follow-up using the interaction ID
followup = client.task_run.create(
input="Which of these works best for enterprise customers?",
processor="core",
previous_interaction_id=task_run.interaction_id,
task_spec=TaskSpecParam(output_schema=TextSchemaParam()),
)
followup_result = client.task_run.result(followup.run_id, api_timeout=3600)
```
```typescript TypeScript theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel();
async function pollResult(runId: string) {
for (let i = 0; i < 20; i++) {
try {
return await client.taskRun.result(runId, { timeout: 25 });
} catch (error) {
if (i === 19) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
// Initial request
const taskRun = await client.taskRun.create({
input: "What are the main pricing models used by SaaS companies?",
processor: "core",
task_spec: { output_schema: { type: "text" } },
});
const result = await pollResult(taskRun.run_id);
// Follow-up using the interaction ID
const followup = await client.taskRun.create({
input: "Which of these works best for enterprise customers?",
processor: "core",
previous_interaction_id: taskRun.interaction_id,
task_spec: { output_schema: { type: "text" } },
});
const followupResult = await pollResult(followup.run_id);
```
```bash cURL theme={"system"}
# Initial request
RUN_JSON=$(curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"input": "What are the main pricing models used by SaaS companies?",
"processor": "core",
"task_spec": { "output_schema": { "type": "text" } }
}')
INTERACTION_ID=$(echo "$RUN_JSON" | jq -r '.interaction_id')
RUN_ID=$(echo "$RUN_JSON" | jq -r '.run_id')
# Poll for result...
# Follow-up using the interaction ID
curl -s "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"input\": \"Which of these works best for enterprise customers?\",
\"processor\": \"core\",
\"previous_interaction_id\": \"${INTERACTION_ID}\",
\"task_spec\": { \"output_schema\": { \"type\": \"text\" } }
}"
```
## Cross-Processor Interactions
You can chain interactions across different processors of the Task API.
Interactions are **not available** for Zero Data Retention (ZDR) customers.
## Next Steps
* [**Interactive Research:**](/task-api/examples/interactive-research) Build an interactive research app using interactions
* [**Deep Research:**](/task-api/examples/task-deep-research) Comprehensive single-turn research
* [**Enrichment:**](/task-api/examples/task-enrichment) Structured data enrichment
# Task Spec
Source: https://docs.parallel.ai/task-api/guides/specify-a-task
Define structured research tasks with customizable input and output schemas.
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## Task Spec
A Task Specification ([Task Spec](/api-reference/tasks/create-task-run#body-task-spec-output-schema)) is a declarative template that defines the structure and requirements for the outputs of a web research operation. While optional in each Task Run, Task Specs provide significant advantages when you need precise control over your research data.
Task Specs ensure consistent results by enforcing a specific output structure across multiple runs. They validate schema against expected formats and create reusable templates for common research patterns. By defining the expected outputs clearly, they also serve as self-documentation for your tasks, making them easier to understand and maintain.
| Component | Required | Purpose | Format |
| - | - | - | - |
| **Output Schema** | Yes | Defines the structure and fields of the task result | JSON Schema or Text |
| **Input Schema** | No | Specifies expected input parameters and their formats | JSON Schema or Text |
## Task Spec Structure
A Task Spec consists of:
| Field | Type | Required | Description |
| - | - | - | - |
| `output_schema` | Schema object or string | Yes | Description and structure of the desired output |
| `input_schema` | Schema object or string | No | Description and structure of input parameters |
When providing a bare string for `input_schema` or `output_schema`, it's equivalent to a text schema with that string as the description.
## Schema Types
Task Spec supports three schema formats:
When using the [Python SDK](https://pypi.org/project/parallel-web/), Parallel Tasks also support Pydantic objects in Task Spec
`auto` mode enables Deep Research style outputs only in processors `pro` and above. Read more about Deep Research [here](/task-api/examples/task-deep-research).
```json theme={"system"}
{
"json_schema": {
"type": "object",
"properties": {
"population": {
"type": "string",
"description": "Current population with year of estimate"
},
"area": {
"type": "string",
"description": "Total area in square kilometers and square miles"
}
},
"required": ["population", "area"]
},
"type": "json"
}
```
```json theme={"system"}
{
"description": "Summary of the country's economic indicators for 2023",
"type": "text"
}
```
```json theme={"system"}
{
"type": "auto"
}
```
See [Best Practices](/task-api/best-practices) for guidance on writing effective Task Specs.
## Output Schema Validation Rules
The Task API enforces several restrictions on output schemas to ensure compatibility and performance:
### Schema Structure Rules
| Rule | Type | Description |
| - | - | - |
| Root type must be object | error | The root schema must have `"type": "object"` |
| Root must have properties | error | The root object must have a `properties` field |
| Root cannot use anyOf | error | The root level cannot use `anyOf` |
| Standalone null type | error | `null` type is only allowed in union types or anyOf |
| All fields must be required | warning | All properties should be listed in the `required` array |
| additionalProperties must be false | warning | All object types should set `additionalProperties: false` |
While all fields must be required in the schema, you can create optional parameters by using a union type with `null`. For example, `"type": ["string", "null"]` allows a field to be either a string or null, effectively making it optional while maintaining schema compliance.
### Size and Complexity Limits
**25,000 character limit:** The combined length of your task specification AND input data cannot exceed 25,000 characters. This includes:
* All field names and descriptions in your schemas
* The `objective` or `description` text
* Your input data (the entity you're researching)
If you hit this limit, simplify your schema descriptions or reduce input size. For large input data, consider splitting into multiple tasks.
| Rule | Type | Limit | Description |
| - | - | - | - |
| Nesting depth | error | 5 levels | Maximum nesting depth of objects and arrays |
| Total properties | error | 100 | Maximum total number of properties across all levels |
| Total string length | error | 25,000 chars | Maximum total string length for names and values |
| Enum values | error | 500 | Maximum number of enum values across all properties |
| Large enum string length | error | 7,500 chars | Maximum string length for enums with >250 values |
| Task spec size | error | 15,000 chars | Maximum length of the task specification alone |
| **Total size** | error | **25,000 chars** | **Maximum combined length of task specification + input data** |
### Unsupported Keywords
The following JSON Schema keywords are not supported in output schemas:
`contains`, `format`, `maxContains`, `maxItems`, `maxLength`, `maxProperties`, `maximum`, `minContains`, `minItems`, `minLength`, `minimum`, `minProperties`, `multipleOf`, `pattern`, `patternProperties`, `propertyNames`, `uniqueItems`, `unevaluatedItems`, `unevaluatedProperties`
irements: it has an object root type with properties, all fields are required, and `additionalProperties` is set to false.
### Common Schema Errors to Avoid
Here are examples of common schema errors and how to fix them:
```json theme={"system"}
{
"type": "array", // Error: Root type must be "object"
"items": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
}
```
**Fix:** Change the root type to "object" and move array properties to a field:
```json theme={"system"}
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}
}
},
"required": ["items"],
"additionalProperties": false
}
```
```json theme={"system"}
{
"type": "object",
"anyOf": [ // Error: Root level cannot use anyOf
{
"properties": {
"field1": { "type": "string" }
}
},
{
"properties": {
"field2": { "type": "string" }
}
}
]
}
```
**Fix:** Combine the properties into a single object:
```json theme={"system"}
{
"type": "object",
"properties": {
"field1": { "type": "string" },
"field2": { "type": "string" }
},
"required": ["field1", "field2"],
"additionalProperties": false
}
```
```json theme={"system"}
{
"type": "object",
"properties": {
"field1": { "type": "string" },
"field2": { "type": "string" }
},
"required": ["field1"] // Warning: All fields should be required
}
```
**Fix:** Add all fields to the required array:
```json theme={"system"}
{
"type": "object",
"properties": {
"field1": { "type": "string" },
"field2": { "type": "string" }
},
"required": ["field1", "field2"],
"additionalProperties": false
}
```
```json theme={"system"}
{
"type": "object",
"properties": {
"field1": { "type": "string" },
"field2": { "type": "string" }
},
"required": ["field1", "field2"],
"additionalProperties": true // Warning: should be false
}
```
**Fix:** Set additionalProperties to false:
```json theme={"system"}
{
"type": "object",
"properties": {
"field1": { "type": "string" },
"field2": { "type": "string" }
},
"required": ["field1", "field2"],
"additionalProperties": false
}
```
```json theme={"system"}
{
"type": "object",
"properties": {
"field1": {
"type": "string",
"minLength": 5 // Error: Unsupported keyword
}
},
"required": ["field1"],
"additionalProperties": false
}
```
**Fix:** Remove unsupported keywords and use descriptions instead:
```json theme={"system"}
{
"type": "object",
"properties": {
"field1": {
"type": "string",
"description": "A string with at least 5 characters"
}
},
"required": ["field1"],
"additionalProperties": false
}
```
# Ingest API
Source: https://docs.parallel.ai/task-api/ingest-api
API reference for creating awesome tasks
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## API Overview
The Parallel Ingest API provides endpoints for creating intelligent task runs that can perform web research and data extraction. The API is built around a stateful architecture where task creation and result retrieval are separate operations.
## Endpoints
### Suggest Task
`POST /v1beta/tasks/suggest`
Generate a task specification based on user intent. This endpoint helps you create properly structured tasks by analyzing your requirements and suggesting appropriate schemas.
#### Request Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `user_intent` | string | Yes | Natural language description of what you want to accomplish |
| `previous_task` | `SuggestedTaskSpec` | No | Previous task specification to iterate upon and improve, or to restrict input columns to a predefined set (see [example](#select-input-columns-from-a-predefined-set) below) |
#### Response Schema
Returns a `SuggestedTaskSpec` object with the following fields:
| Field | Type | Description |
| - | - | - |
| `input_schema` | object | JSON schema defining expected input structure |
| `output_schema` | object | JSON schema defining expected output structure |
| `inputs` | array | Sample input data, if provided in the user intent |
| `title` | string | Suggested title for the task |
| `warnings` | array | Optional list of warnings about the generated specification |
**Warning Types:**
| Warning Type | Description |
| - | - |
| `schema_generalization` | Some fields were generalized to create a more reusable schema |
| `unparsable_input` | User-provided input data couldn't be fully parsed |
| `unattainable_task` | The requested task cannot be created exactly as specified |
#### Example Request
```bash theme={"system"}
curl -X POST "https://api.parallel.ai/v1beta/tasks/suggest" \
-H "x-api-key: $PARALLEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_intent": "Find the CEOs of tech companies"
}'
```
**With previous task iteration:**
```bash theme={"system"}
curl -X POST "https://api.parallel.ai/v1beta/tasks/suggest" \
-H "x-api-key: $PARALLEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_intent": "I want to also include the company website and founding year in the output schema",
"previous_task": {
"input_schema": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"description": "Name of the company"
}
},
"required": ["company_name"]
},
"output_schema": {
"type": "object",
"properties": {
"ceo_name": {
"type": "string",
"description": "Current CEO of the company"
}
}
}
}
}'
```
```python theme={"system"}
import requests
url = "https://api.parallel.ai/v1beta/tasks/suggest"
headers = {
"x-api-key": "PARALLEL_API_KEY",
"Content-Type": "application/json"
}
data = {
"user_intent": "Find the CEOs of tech companies"
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result)
```
**With previous task iteration:**
```python theme={"system"}
import requests
url = "https://api.parallel.ai/v1beta/tasks/suggest"
headers = {
"x-api-key": "PARALLEL_API_KEY",
"Content-Type": "application/json"
}
data = {
"user_intent": "I want to also include the company website and founding year in the output",
"previous_task": {
"input_schema": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"description": "Name of the company"
}
},
"required": ["company_name"]
},
"output_schema": {
"type": "object",
"properties": {
"ceo_name": {
"type": "string",
"description": "Current CEO of the company"
}
}
}
}
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result)
```
```typescript theme={"system"}
const response = await fetch('https://api.parallel.ai/v1beta/tasks/suggest', {
method: 'POST',
headers: {
"x-api-key": "PARALLEL_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
user_intent: 'Find the CEOs of tech companies'
})
});
const result = await response.json();
console.log(result);
```
**With previous task iteration:**
```typescript theme={"system"}
const response = await fetch('https://api.parallel.ai/v1beta/tasks/suggest', {
method: 'POST',
headers: {
"x-api-key": "PARALLEL_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
user_intent: 'I want to also include the company website and founding year',
previous_task: {
input_schema: {
type: 'object',
properties: {
company_name: {
type: 'string',
description: 'Name of the company'
}
},
required: ['company_name']
},
output_schema: {
type: 'object',
properties: {
ceo_name: {
type: 'string',
description: 'Current CEO of the company'
}
}
}
}
})
});
const result = await response.json();
console.log(result);
```
#### Example Response
```json theme={"system"}
{
"input_schema": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"description": "Name of the company"
}
},
"required": ["company_name"]
},
"output_schema": {
"type": "object",
"properties": {
"ceo_name": {
"type": "string",
"description": "Current CEO of the company"
},
"appointed_date": {
"type": "string",
"description": "Date when the CEO was appointed"
}
}
},
"inputs": [],
"title": "Find Company CEO Information"
}
```
### Suggest Processor
`POST /v1beta/tasks/suggest-processor`
Enhance and optimize a task specification by suggesting the most appropriate processor and refining the schemas.
#### Suggest Processor Request Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `task_spec` | object | Yes | Task specification object to be processed |
| `choose_processors_from` | array | No | List of processors to choose from. If not provided, the API will consider all available processors. |
**Valid values:** `base`, `core`, `core2x`, `pro`, `ultra`, `ultra2x`, `ultra4x`, `ultra8x`
The `lite` processor is available for task execution but will never be returned by this endpoint.
See [Processors](/task-api/guides/choose-a-processor) for details on each processor.
#### Suggest Processor Example Request
```bash theme={"system"}
curl -X POST "https://api.parallel.ai/v1beta/tasks/suggest-processor" \
-H "x-api-key: $PARALLEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"task_spec": {
"input_schema": {
"type": "object",
"properties": {
"company_name": {
"type": "string"
}
}
},
"output_schema": {
"type": "object",
"properties": {
"ceo_name": {
"type": "string"
}
}
}
},
"choose_processors_from": ["base", "core", "core2x", "pro", "ultra"]
}'
```
```python theme={"system"}
import requests
url = "https://api.parallel.ai/v1beta/tasks/suggest-processor"
headers = {
"x-api-key": "PARALLEL_API_KEY",
"Content-Type": "application/json"
}
data = {
"task_spec": {
"input_schema": {
"type": "object",
"properties": {
"company_name": {
"type": "string"
}
}
},
"output_schema": {
"type": "object",
"properties": {
"ceo_name": {
"type": "string"
}
}
}
},
"choose_processors_from": ["base", "core", "core2x", "pro", "ultra"]
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result)
```
```typescript theme={"system"}
const response = await fetch('https://api.parallel.ai/v1beta/tasks/suggest-processor', {
method: 'POST',
headers: {
"x-api-key": "PARALLEL_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
task_spec: {
input_schema: {
type: 'object',
properties: {
company_name: {
type: 'string'
}
}
},
output_schema: {
type: 'object',
properties: {
ceo_name: {
type: 'string'
}
}
}
},
choose_processors_from: ["base", "core", "core2x", "pro", "ultra"]
})
});
const result = await response.json();
console.log(result);
```
#### Suggest Processor Response Schema
| Field | Type | Description |
| - | - | - |
| `recommended_processors` | array | List of recommended processors in priority order. We recommend using the first element. |
Returns an enhanced task specification with additional fields and optimizations.
#### Suggest Processor Example Response
```json theme={"system"}
{
"recommended_processors": ["pro"]
}
```
#### How Processor Suggestion Works
The `/suggest-processor` endpoint analyzes your task specification to recommend the most appropriate processor. The algorithm considers:
1. **Task Complexity** - Number of output fields, depth of research required
2. **Research Pattern** - Whether the task requires single-step lookups, multi-step reasoning, or parallel breadth-first research
3. **Data Sources** - How many disparate sources need to be consulted
4. **Special Tools** - Whether the task requires specialized capabilities like entity ranking
The recommendation balances task requirements against processor capabilities, selecting the lowest-cost processor that can reliably complete your task.
The first processor in `recommended_processors` is always the best recommendation. The API may return multiple processors if several could handle the task, but we recommend using the first one.
## Examples
### Select Input Columns from a Predefined Set
Sometimes you have a specific dataset with fixed columns and need to create a task that works exclusively with those columns. The `previous_task` parameter allows you to constrain the API to generate task specifications that match your exact data structure.
**When to use this approach:**
* You have a fixed dataset schema that cannot be modified
* You want to ensure the task only uses your specific input columns
* You need to provide examples that match your exact data format
* You want to prevent the API from suggesting additional input fields
**The workflow:**
1. **Define Your Schema**: Specify exactly which columns you want to use as inputs with their descriptions
2. **Provide Sample Data**: Include examples that match your exact data format
3. **Generate a `SuggestedTaskSpec`**: Use the helper function to create a properly formatted `SuggestedTaskSpec` object
4. **Refine with API**: Pass this as `previous_task` to get a refined task spec that respects your column constraints
The API will use your predefined input schema as a foundation and refine the output schema while preserving your input columns. This guarantees the final task specification integrates seamlessly with your existing dataset.
```python [expandable] theme={"system"}
import requests
import json
if __name__ == "__main__":
user_intent = "Find the CEO, investments, and customer details for the company"
columns_with_descriptions = [
("company_id", "The unique identifier of the company to retrieve executive, investment, and customer details for."),
("company_name", "The name of the company to identify and gather detailed information about."),
("company_website", "The domain of the company's website to assist in identifying the correct organization."),
("industry", "The primary industry the company operates in."),
("employee_count", "The exact number of employees at the company.")
]
examples = [
{
"company_id": "comp_001",
"company_name": "Parallel AI",
"company_website": "parallel.ai",
"industry": "AI",
"employee_count": "25"
},
{
"company_id": "comp_002",
"company_name": "Google",
"company_website": "google.com",
"industry": "Software",
"employee_count": "125000"
}
]
def get_suggested_task_spec(columns_with_descriptions, examples, title):
all_valid_columns = {
column_name: {
"type": "string",
"description": description
}
for column_name, description in columns_with_descriptions
}
return {
"input_schema": {
"type": "object",
"properties": all_valid_columns
},
"output_schema": {
"type": "object",
"properties": {
"answer": {
"type": "string",
"description": "answer to the question"
}
},
"required": ["answer"],
},
"inputs": examples,
"title": title
}
suggested_task_spec = get_suggested_task_spec(
columns_with_descriptions=columns_with_descriptions,
examples=examples,
title="Company executive, investments, and customer details"
)
url = "https://api.parallel.ai/v1beta/tasks/suggest"
headers = {
"x-api-key": "PARALLEL_API_KEY",
"Content-Type": "application/json"
}
data = {
"user_intent": f"{user_intent}. Improve output_schema to include more descriptive fields, and only keep input fields that are relevant to answering the question.",
"previous_task": suggested_task_spec
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(json.dumps(result, indent=2))
```
### End-to-End Ingest to Task Execution
The following Python script demonstrates the complete workflow of the Ingest API, from task suggestion to result retrieval:
```python [expandable] theme={"system"}
#!/usr/bin/env python3
"""
End-to-end test script for Parallel Ingest API
This script demonstrates the complete workflow:
1. Suggest a task based on user intent
2. Suggest a processor for the task
3. Create and run the task
4. Retrieve the results
Usage:
python test_ingest_api.py
Make sure to set your PARALLEL_API_KEY environment variable or update the script directly.
"""
import os
import requests
import json
import time
from typing import Dict, Any, Optional
# Configuration
API_KEY = "PARALLEL_API_KEY"
BASE_URL = "https://api.parallel.ai"
class IngestAPITester:
def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.base_url = base_url
self.headers = {
"x-api-key": api_key,
"Content-Type": "application/json"
}
def suggest_task(self, user_intent: str) -> Optional[Dict[str, Any]]:
"""Step 1: Suggest a task based on user intent"""
print(f"🔍 Step 1: Suggesting task for intent: '{user_intent}'")
url = f"{self.base_url}/v1beta/tasks/suggest"
data = {"user_intent": user_intent}
try:
response = requests.post(url, headers=self.headers, json=data)
response.raise_for_status()
result = response.json()
print("✅ Task suggestion successful!")
print(f" Title: {result.get('title', 'N/A')}")
print(f" Input schema: {json.dumps(result.get('input_schema', {}), indent=2)}")
print(f" Output schema: {json.dumps(result.get('output_schema', {}), indent=2)}")
print()
return result
except requests.exceptions.RequestException as e:
print(f"❌ Error suggesting task: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f" Response: {e.response.text}")
return None
def suggest_processor(self, task_spec: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Step 2: Suggest a processor for the task"""
print("🔧 Step 2: Suggesting processor for the task")
url = f"{self.base_url}/v1beta/tasks/suggest-processor"
data = {
"task_spec": task_spec,
"choose_processors_from": ["base", "core", "core2x", "pro", "ultra"]
}
try:
response = requests.post(url, headers=self.headers, json=data)
response.raise_for_status()
result = response.json()
print("✅ Processor suggestion successful!")
# Extract the first recommended processor
recommended_processors = result.get('recommended_processors', [])
if recommended_processors:
selected_processor = recommended_processors[0]
print(f" Recommended processors: {recommended_processors}")
print(f" Selected processor: {selected_processor}")
result['selected_processor'] = selected_processor
else:
print(" ⚠️ No processors recommended, defaulting to 'core'")
result['selected_processor'] = 'core'
print(f" Enhanced task spec received")
print()
return result
except requests.exceptions.RequestException as e:
print(f"❌ Error suggesting processor: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f" Response: {e.response.text}")
return None
def create_task_run(self, input_data: Any, processor: str = "core", task_spec: Optional[Dict] = None) -> Optional[str]:
"""Step 3: Create a task run"""
print(f"🚀 Step 3: Creating task run with processor '{processor}'")
url = f"{self.base_url}/v1/tasks/runs"
data = {
"input": input_data,
"processor": processor
}
if task_spec:
# Format the task_spec according to the documentation
# Schemas need to be wrapped with type and json_schema fields
formatted_task_spec = {}
if "input_schema" in task_spec:
formatted_task_spec["input_schema"] = {
"type": "json",
"json_schema": task_spec["input_schema"]
}
if "output_schema" in task_spec:
formatted_task_spec["output_schema"] = {
"type": "json",
"json_schema": task_spec["output_schema"]
}
data["task_spec"] = formatted_task_spec
try:
response = requests.post(url, headers=self.headers, json=data)
response.raise_for_status()
result = response.json()
run_id = result.get("run_id")
status = result.get("status")
print(f"✅ Task run created successfully!")
print(f" Run ID: {run_id}")
print(f" Status: {status}")
print()
return run_id
except requests.exceptions.RequestException as e:
print(f"❌ Error creating task run: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f" Response: {e.response.text}")
return None
def get_task_result(self, run_id: str, max_attempts: int = 30, wait_time: int = 10) -> Optional[Dict[str, Any]]:
"""Step 4: Get task results (with polling)"""
print(f"📊 Step 4: Retrieving results for run {run_id}")
url = f"{self.base_url}/v1/tasks/runs/{run_id}/result"
headers = {"x-api-key": self.api_key} # No Content-Type needed for GET
for attempt in range(max_attempts):
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
result = response.json()
status = result.get("run", {}).get("status")
if status == "completed":
print("✅ Task completed successfully!")
output = result.get("output", {})
print(f" Content: {output.get('content', 'N/A')}")
# Show citations if available
citations = output.get("citations", [])
if citations:
print(f" Citations: {len(citations)} sources")
for i, citation in enumerate(citations[:3], 1): # Show first 3
print(f" {i}. {citation}")
return result
elif status == "failed":
print("❌ Task failed!")
return result
else:
print(f"⏳ Task still {status}... (attempt {attempt + 1}/{max_attempts})")
time.sleep(wait_time)
elif response.status_code == 404:
print(f"❌ Task run not found: {run_id}")
return None
else:
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"❌ Error getting task result: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f" Response: {e.response.text}")
return None
print(f"⏰ Task did not complete within {max_attempts * wait_time} seconds")
return None
def run_end_to_end_test(self, user_intent: str, sample_input: Any):
"""Run the complete end-to-end test"""
print("=" * 60)
print("🧪 PARALLEL INGEST API - END-TO-END TEST")
print("=" * 60)
print()
# Step 1: Suggest task
task_suggestion = self.suggest_task(user_intent)
if not task_suggestion:
print("❌ Test failed at task suggestion step")
return
# Step 2: Suggest processor
processor_suggestion = self.suggest_processor(task_suggestion)
if not processor_suggestion:
print("❌ Test failed at processor suggestion step")
return
# Step 3: Create task run
selected_processor = processor_suggestion.get('selected_processor', 'core')
run_id = self.create_task_run(
input_data=sample_input,
processor=selected_processor,
task_spec=task_suggestion # Use original task suggestion, not processor suggestion
)
if not run_id:
print("❌ Test failed at task creation step")
return
# Step 4: Get results
result = self.get_task_result(run_id)
if result:
print("🎉 End-to-end test completed successfully!")
else:
print("❌ Test failed at result retrieval step")
def main():
"""Main function to run the test"""
# Check API key
if API_KEY == "PARALLEL_API_KEY":
print("⚠️ Please set your PARALLEL_API_KEY environment variable or update the script")
print(" Example: export PARALLEL_API_KEY=your_actual_api_key")
return
# Initialize tester
tester = IngestAPITester(API_KEY, BASE_URL)
# Test configuration
user_intent = "Given company_name and company_website, find the CEO information for technology companies"
# Use object input that matches the expected schema
sample_input = {
"company_name": "Google",
"company_website": "https://www.google.com"
}
# Run the test
tester.run_end_to_end_test(user_intent, sample_input)
if __name__ == "__main__":
main()
```
Running the Example
```bash theme={"system"}
PARALLEL_API_KEY="PARALLEL_API_KEY" python3 ingest_script.py
```
This example demonstrates the complete workflow:
1. **Suggest Task**: Generate a task specification from natural language intent
2. **Suggest Processor**: Get processor recommendations and enhanced schemas
3. **Create Task Run**: Submit the task for processing with proper schema formatting
4. **Get Results**: Poll for completion and retrieve the final results
The script includes proper error handling, status polling, and demonstrates the correct format for task specifications required by the API.
# MCP Tool Calling
Source: https://docs.parallel.ai/task-api/mcp-tool-call
Using MCP servers for tool calls in Tasks
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## Overview
The Parallel API allows you to specify remote MCP servers for Task API execution. This enables the model to access tools hosted on remote MCP servers without needing a separate MCP client.
This page covers MCP servers you supply, including providers your organization already licenses. For Data Connectors that Parallel manages, see [Data Connectors](/resources/data-connectors).
### Specifying MCP Servers
MCP servers are specified using the `mcp_servers` field in the Task API call. Each request can include up to 10 MCP servers.
| Parameter | Type | Description |
| - | - | - |
| `type` | `string` | Always `url`. |
| `url` | `string` | The URL of the MCP server. |
| `name` | `string` | A name for the MCP server. |
| `headers` | `dict[string, string]` | Headers for authenticating with the MCP server. |
| `allowed_tools` | `array[string]` or `null` | List of tools to allow, or null for all. |
#### Sample Request
```bash Task API theme={"system"}
curl -X POST "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: $PARALLEL_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"input": "What is the latest in AI research?",
"processor": "lite",
"mcp_servers": [
{
"type": "url",
"url": "https://dummy_mcp_server",
"name": "dummy_mcp_server",
"headers": {"x-api-key": "API_KEY"}
}
]
}'
```
#### Restrictions
* Only MCP servers with Streamable HTTP transport are currently supported.
* From the [MCP specification](https://modelcontextprotocol.io/specification/2025-03-26), only tools are supported.
* For [MCP servers using OAuth](https://modelcontextprotocol.io/specification/draft/basic/authorization), you must generate the authorization token separately and include it as a bearer token in the headers.
* You can specify up to 10 MCP servers per request, but using fewer is recommended for optimal result quality.
## Using MCP Servers in the Task API
When you make a Task API request, the API first fetches the available tools from the specified MCP servers.
The processor will invoke tools from these servers if it determines they are useful for the task. The number of tool calls depends
on the [processor](/task-api/guides/choose-a-processor):
* For `lite` and `core`, at most one tool is invoked.
* For all other processors, multiple tool calls may be made.
## Response Content
The Task API response includes a list of tool calls made during execution. Each tool call entry contains:
| Parameter | Type | Description |
| - | - | - |
| `tool_call_id` | `string` | Unique identifier for the tool call. |
| `server_name` | `string` | Name of the MCP server, as provided in the input. |
| `tool_name` | `string` | Name of the tool invoked. |
| `arguments` | `string` | JSON-encoded string of the arguments used for the tool call. |
| `content` | `string` | Response from the MCP server. |
| `error` | `string` | Error message if the tool call failed. Either `content` or `error` will always be populated. |
If there is an authentication issue with any MCP server, the top-level `warning` field in the Task Run output
will be populated.
```bash Success theme={"system"}
{
"run": {
"run_id": "trun_0cb15e174bc44a019a58b8a02ebefe54",
"interaction_id": "trun_0cb15e174bc44a019a58b8a02ebefe54",
"status": "completed",
"is_active": false,
"processor": "lite",
"metadata": {},
"created_at": "2026-09-30T18:37:31.805687Z",
"modified_at": "2026-09-30T18:38:49.550141Z"
},
"output": {
"basis": [
{
"field": "output",
"citations": [
{
"title": "Reimagining research papers as interactive and reliable AI agents - PubMed",
"url": "https://pubmed.ncbi.nlm.nih.gov/42749808",
"excerpts": [
"Paper2Agent transforms research output from passive artefacts into active systems that accelerate use and discovery. ... Paper2Agent addresses this challenge by converting a paper into an AI agent ..."
]
}
],
"reasoning": "The PubMed abstract describes Paper2Agent as turning papers into active AI agents, including access to research materials and the ability to answer natural-language questions while invoking paper workflows. Stanford Medicine’s September 16 report describes ...",
"confidence": "medium"
}
],
"type": "json",
"mcp_tool_calls": [
{
"tool_call_id": "call_rrlBJ4VpZYl7VkYqosVkGKjD",
"server_name": "parallel_web_search",
"tool_name": "web_search",
"arguments": "{\"objective\": \"Identify notable, very recent AI research findings and publications available as of September 30, 2026.\", \"search_queries\": [\"AI research breakthroughs September 2026\", \"latest AI research papers September 2026 arXiv\", \"AI lab research announcements September 2026\"]}",
"content": "{\n \"search_id\": \"search_cc65ce8e28c50dff38b30b57e79e1230\",\n \"results\": ...}",
"error": ""
},
{
"tool_call_id": "call_8XO1rg0mLd1J8TntcykQJoa9",
"server_name": "parallel_web_search",
"tool_name": "web_search",
"arguments": "{\"objective\": \"Find primary-source AI research papers or lab research announcements published in the last week of September 2026.\", \"search_queries\": [\"AI research paper September 29 2026\", \"AI research announcement September 2026 lab\", \"AI science breakthrough September 2026\"]}",
"content": "{\n \"search_id\": \"search_f27ffd70902ddbf1922b28fe9442c224\",\n \"results\": ...}",
"error": ""
}
],
"content": {
"output": "Notable recent AI-research developments in September 2026:\n- **Paper2Agent:** A framework that turns scientific papers into interactive AI agents, with access ..."
},
"output_schema": {}
}
}
```
```bash Failure authenticating to MCP server theme={"system"}
{
"run": {
"run_id": "trun_0cb15e174bc44a01a523e494f20f0a3a",
"interaction_id": "trun_0cb15e174bc44a01a523e494f20f0a3a",
"status": "completed",
"is_active": false,
"warnings": [
{
"type": "warning",
"message": "Error listing tools from MCP server dummy_mcp_server: 502 Bad gateway. Server details: Name: dummy_mcp_server, URL: https://dummy_mcp_server, Headers: {\"x-api-key\": \"REDACTED\"}. Reference ID: 5e87a738-16e4-43a1-ba35-3787d0a75d1d",
"detail": {}
}
],
"processor": "lite",
"metadata": {},
"created_at": "2026-09-30T18:36:02.302876Z",
"modified_at": "2026-09-30T18:36:37.756581Z"
},
"output": {
"basis": [
{
"field": "output",
"citations": [
{
"title": "AI system helps lab devices 'talk' with each other — streamlining research | Nature",
"url": "https://www.nature.com/articles/d41586-026-02990-8",
"excerpts": [
"nature news article NEWS 24 September 2026 AI system helps lab devices 'talk' with each other — streamlining research Platform allows disparate machines to communicate and to be controlled by an ..."
]
}
],
"reasoning": "The cited Nature passage identifies the September 24 lab-device platform and its AI-agent functions. The Paper2Agent passage describes making research papers and their materials usable through agents and workflows. The scientific-ideas study reports both the ...",
"confidence": "medium"
}
],
"type": "json",
"content": {
"output": "As of September 30, 2026, notable recent directions in AI research include:\n\n- **AI-enabled lab automation:** A September 24 report described a system ..."
},
"output_schema": {}
}
}
```
# Source Policy
Source: https://docs.parallel.ai/task-api/source-policy
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
# Task MCP
Source: https://docs.parallel.ai/task-api/task-mcp
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
# Task API Quickstart
Source: https://docs.parallel.ai/task-api/task-quickstart
Transform complex knowledge work into programmable, repeatable operations powered by AI web research
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
The **Task API** combines AI inference with web search and live crawling to turn complex research tasks into repeatable workflows. Define what you need in plain language or JSON, and the Task API handles the research, synthesis, and structured output—complete with citations and confidence levels.
See [Pricing](/getting-started/pricing) for a detailed schedule of rates.
## What you can build
The Task API is designed for maximum extensibility. Create a task spec for any research need:
* **Data enrichment**: Enhance CRM records, company databases, or contact lists with web intelligence
* **Market research**: Generate comprehensive reports on industries, competitors, or trends
* **Due diligence**: Automate compliance checks, background research, and verification workflows
* **Content generation**: Create research-backed reports, summaries, and analyses
## Prerequisites
Generate your API key on [Platform](https://platform.parallel.ai). Then, set up with the TypeScript SDK, Python SDK or with cURL:
```bash cURL theme={"system"}
echo "Install curl and jq via brew, apt, or your favorite package manager"
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
```bash Python theme={"system"}
pip install parallel-web
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
```bash TypeScript theme={"system"}
npm install parallel-web
export PARALLEL_API_KEY="PARALLEL_API_KEY"
```
## Quick start
Every Task API workflow follows three steps: **create** a task run, **wait** for completion, and **retrieve** the result.
```python theme={"system"}
from parallel import Parallel
client = Parallel(api_key="PARALLEL_API_KEY")
# 1. Create a task run
task_run = client.task_run.create(
input="Stripe",
task_spec={"output_schema": "The founding year and total funding raised"},
processor="base"
)
# 2-3. Retrieve the result (blocks until complete)
run_result = client.task_run.result(task_run.run_id, api_timeout=3600)
print(run_result.output)
```
For complete end-to-end examples with all languages, polling, and response handling, see:
Enrich structured data with web intelligence — includes cURL, Python, TypeScript, and async examples
Generate comprehensive reports — includes polling, webhooks, and SSE approaches
## Core concepts
Before diving in, understand these key concepts:
Define your research task using input/output schemas in plain language or JSON
Choose the right processor tier based on research depth and latency requirements
Every output includes citations, reasoning, and confidence levels for verification
### Output schema types
The Task API supports four output schema types:
| Type | Description | When to Use |
| - | - | - |
| **Text string** | Plain text description (e.g., `"The founding date in MM-YYYY format"`) | Simple lookups, single-field answers |
| **JSON schema** | `{"type": "json", "json_schema": {...}}` | Structured enrichment with multiple typed fields |
| **Text schema** | `{"type": "text"}` with optional `description` | Markdown reports with inline citations |
| **Auto** | `{"type": "auto"}`, or omit `task_spec` entirely | Let the processor determine the best output structure |
See [Specify a Task](/task-api/guides/specify-a-task) for schema best practices and [Processors](/task-api/guides/choose-a-processor) for choosing the right processor tier. For Python SDK users, these correspond to `TaskSpecParam`, `JsonSchemaParam`, and `TextSchemaParam` types from `parallel.types`.
## Input and output patterns
The Task API supports flexible input/output combinations to match your use case:
### Question in → Answer out
The simplest pattern: ask a question, get a researched answer.
```python theme={"system"}
task_run = client.task_run.create(
input="What is the founding date of the United Nations?",
task_spec={"output_schema": "The founding date in MM-YYYY format"},
processor="base"
)
# Output: "10-1945"
```
### Question in → Report out
Generate comprehensive markdown reports with inline citations.
```python theme={"system"}
from parallel.types import TaskSpecParam, TextSchemaParam
task_run = client.task_run.create(
input="Create a market research report on the HVAC industry in the USA",
processor="ultra",
task_spec=TaskSpecParam(output_schema=TextSchemaParam())
)
# Output: Multi-page markdown report with citations
```
### Question in → Auto-structured output
Let the processor automatically determine the best output structure.
```python theme={"system"}
task_run = client.task_run.create(
input="Research the top 5 AI infrastructure companies and their recent funding",
processor="ultra"
)
# Output: Automatically structured JSON with company profiles, funding details, etc.
```
### Structured input → Structured output
Define explicit input and output schemas for precise control over data enrichment.
```python theme={"system"}
task_run = client.task_run.create(
input={"company_name": "Stripe", "website": "stripe.com"},
task_spec={
"input_schema": {
"type": "json",
"json_schema": {
"type": "object",
"properties": {
"company_name": {"type": "string"},
"website": {"type": "string"}
}
}
},
"output_schema": {
"type": "json",
"json_schema": {
"type": "object",
"properties": {
"founding_year": {"type": "string"},
"employee_count": {"type": "string"},
"total_funding": {"type": "string"}
}
}
}
},
processor="core"
)
```
## Use cases
Enhance structured data with web intelligence. Start with a spreadsheet or database, add new columns with researched data.
Conduct open-ended research without structured input. Generate comprehensive reports on any topic.
## Next steps
* [**Enrichment quickstart:**](/task-api/examples/task-enrichment) Learn how to enrich structured data at scale
* [**Deep Research quickstart:**](/task-api/examples/task-deep-research) Generate comprehensive research reports
* [**Task Groups:**](/task-api/group-api) Run multiple tasks concurrently with batch tracking
* [**Streaming Events:**](/task-api/task-sse) Monitor long-running tasks with real-time progress updates
* [**Webhooks:**](/task-api/webhooks) Configure HTTP callbacks for task completion notifications
* [**API Reference:**](/api-reference/tasks/create-task-run) Complete endpoint documentation
## Rate limits
See [Rate Limits](/resources/rate-limits) for default quotas and how to request higher limits.
# Task API Streaming Events
Source: https://docs.parallel.ai/task-api/task-sse
SSE for Task Runs
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
## Overview
Task Runs support Server-Sent Events (SSE) at the run level, allowing you to receive real-time
updates on ongoing research conducted by our processors during execution.
For streaming events related to Task Groups, see the [streaming endpoints on the Task Group API](./group-api#5-retrieve-results).
Task Group streams provide aggregate group status updates and non-active run state changes. Task Run streams provide detailed events for individual task runs.
For a more comprehensive list of differences, [see here.](#differences-between-task-group-events-and-task-run-events)
### Enabling Events Streaming
To enable periodic event publishing for a task run, set the `enable_events` flag to `true`
when creating the task run. If not specified, events may still be available, but frequent updates are not guaranteed.
Create a Task Run with events aggregation enabled explicitly:
```bash Task API theme={"system"}
curl -X POST "https://api.parallel.ai/v1/tasks/runs" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Content-Type: application/json" \
--data '{
"input": "What is the latest in AI research?",
"processor": "lite",
"enable_events": true
}'
```
To access the event stream for a specific run, use the `/v1/tasks/runs/{run_id}/events` endpoint:
```bash Access event stream theme={"system"}
curl -X GET "https://api.parallel.ai/v1/tasks/runs/trun_6eb64c73e4324b15af2a351bef6d0190/events" \
-H "x-api-key: ${PARALLEL_API_KEY}" \
-H "Accept: text/event-stream"
```
This is what a sample stream looks like:
```bash Event stream theme={"system"}
event: task_run.state
data: {"type":"task_run.state","event_id":null,"input":null,"run":{"run_id":"trun_aa9c7a780c9d4d4b9aa0ca064f61a6f7","interaction_id":"trun_aa9c7a780c9d4d4b9aa0ca064f61a6f7","status":"running","is_active":true,"warnings":null,"error":null,"processor":"pro","metadata":{},"taskgroup_id":null,"created_at":"2025-08-06T00:52:58.619503Z","modified_at":"2025-08-06T00:52:59.495063Z"},"output":null}
event: task_run.progress_msg.exec_status
data: {"type":"task_run.progress_msg.exec_status","message":"Starting research","timestamp":"2025-08-06T00:52:59.786126Z"}
event: task_run.progress_msg.plan
data: {"type":"task_run.progress_msg.plan","message":"I'm working on gathering information about Google's hiring in 2024, including where most jobs were created and any official announcements. I'll review recent news, reports, and Google's own statements to provide a comprehensive answer.","timestamp":"2025-08-06T00:53:19.281306Z"}
event: task_run.progress_msg.tool_call
data: {"type":"task_run.progress_msg.tool_call","message":"I've looked into Google's hiring activity in 2024, focusing on locations and official statements. I'll compile the findings and share a clear update with you shortly.","timestamp":"2025-08-06T00:53:28.282905Z"}
event: task_run.progress_msg.search
data: {"type":"task_run.progress_msg.search","message":"Objective: Find where Google created the most jobs in 2024","timestamp":"2025-08-06T00:53:30.114920Z"}
event: task_run.progress_msg.search
data: {"type":"task_run.progress_msg.search","message":"Query: Google hiring 2024","timestamp":"2025-08-06T00:53:30.114981Z"}
event: task_run.progress_msg.search
data: {"type":"task_run.progress_msg.search","message":"Query: Google 2024 job openings by location","timestamp":"2025-08-06T00:53:30.115044Z"}
event: task_run.progress_stats
data: {"type":"task_run.progress_stats","source_stats":{"num_sources_considered":223,"num_sources_read":22,"sources_read_sample":["http://stcloudlive.com/business/19-layoffs-coming-in-mid-march-at-st-cloud-arctic-cat-facility-company-says","http://snowgoer.com/snowmobiles/arctic-cat-sleds/putting-the-arctic-cat-layoffs-production-stop-in-context/32826","http://25newsnow.com/2024/07/26/cat-deere-cyclical-layoff-mode-say-industry-experts","http://citizen.org/article/big-tech-lobbying-update","http://businessalabama.com/women-in-tech-23-for-23","http://itif.org/publications/2019/10/28/policymakers-guide-techlash","http://distributech.com/","http://newyorker.com/magazine/2019/09/30/four-years-in-startups"]},"progress_meter":50.0}
...
```
**Notes:**
* All [Task API processors](/task-api/guides/choose-a-processor) starting from `pro` and above have event streaming enabled by default.
* Event streams remain open for up to 570 seconds and close earlier when the run becomes non-active.
## Stream Behavior
While a run is active, starting or reconnecting to its stream replays the reasoning messages recorded so far before continuing with new updates. This lets clients reconnect without persisting every reasoning message themselves. Non-active runs behave differently, as described below.
### For Running Tasks
When connecting to a stream for a task that is still running:
* **Complete reasoning trace:** You receive all reasoning messages (`task_run.progress_msg.*`) from the beginning of the task execution, regardless of when you connect to the stream
* **Latest progress stats:** You receive only the current aggregate state via `task_run.progress_stats` events, not historical progress snapshots
* **Real-time updates:** As the task continues, you'll receive new reasoning messages and updated progress statistics
* **Closing status:** Before the connection closes—because the run became non-active or the 570-second window ended—the stream emits a `task_run.state` event. If the run completed successfully, the event includes the complete output
### For Non-Active Tasks
When connecting to a run that is already `completed`, `failed`, `cancelled`, or `action_required`:
* **Current status only:** You receive one `task_run.state` event with the run's current status
* **Immediate result:** If the run completed successfully, that status event includes the complete task output in the `output` field, so you don't also need to use the result endpoint
* **No trace replay:** Reasoning messages and progress statistics are not replayed after the run becomes non-active
### Reconnection Behavior
* Event streams are **not resumable** - there are no sequence numbers or cursors to resume from a specific point
* If you disconnect and reconnect to the same task:
* **Running tasks:** You get the reasoning trace recorded so far plus current progress stats
* **Non-active tasks:** You get the current status event only
* Every connection starts with a `task_run.state` event indicating the current status
### Supported Events
Currently, four types of events are supported:
* **Run Status Events (`task_run.state`):** Indicate the current status of the run. These are sent at the beginning of every stream and when the run transitions to a non-active status.
* **Progress Statistics Events (`task_run.progress_stats`):** Provide point-in-time updates on the number of sources considered and other aggregate statistics. Only the current state is provided, not historical snapshots.
* **Message Events (`task_run.progress_msg.*`):** Communicate progress at various stages of task run execution. While progress data is available, the sequence from the beginning of execution is provided. Subtypes include `.plan` (planning), `.tool_call` (tool use), `.result` (intermediate findings), `.exec_status` (execution status), and `.search` (search activity).
* **Search events (`task_run.progress_msg.search`)** record what the run searched for. Each `message` is prefixed `Objective:` (the goal being researched at that step) or `Query:` (a web search query that was run). A search step typically produces one `Objective:` line followed by one or more `Query:` lines. Reasoning messages such as `.plan` / `.tool_call` may be limited on `lite`; search events are emitted on `base` and above.
* **Error Events (`error`):** Report errors that occur during execution.
Surfaced search queries reflect what the engine searched for at each step, for auditability and observability. When a step issues no explicit query, only the `Objective:` line appears.
**Additional Notes:**
* Event streams always start and end with a status event; for an already non-active run, one event serves as both
* The final status event for completed tasks always includes the complete output in the `output` field
* Events within the reasoning trace maintain their original timestamps, allowing you to understand the execution timeline
* After a run becomes non-active, reconnecting to the event stream returns its current status but does not replay the reasoning trace.
For the full specification of each event, see the examples above.
### Differences Between Task Group Events and Task Run Events
Task Group events are not a strict superset of Task Run events. See the differences below:
| | Task Run Events | Task Group Events |
| - | - | - |
| **Purpose** | Events for a single Task Run. | Events for an entire Task Group. |
| **Run-level events** | Progress updates, messages, status changes. | Only non-active run status changes. |
| **Resumable streams** | No | Yes, using `event_id`. |
| **Events supported** | Progress updates, messages, status changes, and errors for an individual run. | Group status, non-active run state changes, and stream errors. |
| **Reasoning trace** | Available while the run is active; not replayed after it becomes non-active. | Not available. |
| **Final results** | Included in the status event when the run completes successfully. | Available through separate API. |
# Task API Webhooks
Source: https://docs.parallel.ai/task-api/webhooks
Webhook events for task run completions
For AI agents: a documentation index is available at [https://docs.parallel.ai/llms.txt](https://docs.parallel.ai/llms.txt). The full text of all docs is at [https://docs.parallel.ai/llms-full.txt](https://docs.parallel.ai/llms-full.txt). You may also fetch any page as Markdown by appending `.md` to its URL or sending `Accept: text/markdown`.
For Search and Extract, use `/v1/search` and `/v1/extract` for all new integrations and agent tool calls. Use `/v1beta/search` and `/v1beta/extract` only when maintaining an existing integration that already uses them. The request bodies changed too: on `/v1/extract`, `full_content`, `fetch_policy` and `excerpt_settings` go inside `advanced_settings`, and sending the v1beta top-level `excerpts`, `full_content` and `fetch_policy` to `/v1/extract` returns a 422. Do not substitute `/v1` for the documented FindAll or Ingest endpoint paths.
**Prerequisites:** Before implementing Task API webhooks, read **[Webhook Setup & Verification](/resources/webhook-setup)** for critical information on:
* Recording your webhook secret
* Verifying HMAC signatures
* Security best practices
* Retry policies
This guide focuses on Task API-specific webhook events and payloads.
## Overview
Webhooks allow you to receive real-time notifications when your task runs complete, eliminating the need for constant polling—especially useful for long-running or research-intensive tasks.
## Setup
To register a webhook for a task run, include a `webhook` parameter in your task run creation request:
```bash cURL theme={"system"}
curl --request POST \
--url https://api.parallel.ai/v1/tasks/runs \
--header "Content-Type: application/json" \
--header "x-api-key: $PARALLEL_API_KEY" \
--data '{
"task_spec": {
"output_schema": "Find the GDP of the specified country and year"
},
"input": "France (2023)",
"processor": "core",
"metadata": {
"key": "value"
},
"webhook": {
"url": "https://your-domain.com/webhooks/parallel",
"event_types": ["task_run.status"]
}
}'
```
```typescript TypeScript (SDK) theme={"system"}
import Parallel from "parallel-web";
const client = new Parallel({
apiKey: process.env["PARALLEL_API_KEY"],
});
const taskRun = await client.taskRun.create({
task_spec: {
output_schema: "Find the GDP of the specified country and year",
},
input: "France (2023)",
processor: "core",
metadata: {
key: "value",
},
webhook: {
url: "https://your-domain.com/webhooks/parallel",
event_types: ["task_run.status"],
},
});
console.log(taskRun.run_id);
```
```python Python theme={"system"}
import requests
url = "https://api.parallel.ai/v1/tasks/runs"
headers = {
"Content-Type": "application/json",
"x-api-key": "PARALLEL_API_KEY",
}
payload = {
"task_spec": {
"output_schema": "Find the GDP of the specified country and year"
},
"input": "France (2023)",
"processor": "core",
"metadata": {
"key": "value"
},
"webhook": {
"url": "https://your-domain.com/webhooks/parallel",
"event_types": ["task_run.status"],
}
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
### Webhook Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `url` | string | Yes | Your webhook endpoint URL. Can be any domain. |
| `event_types` | array\[string] | Yes | Currently only `["task_run.status"]` is supported. |
## Event Types
Task API currently supports the following webhook event type:
| Event Type | Description |
| - | - |
| `task_run.status` | Emitted when a task run completes (success or failure) |
## Webhook Payload Structure
Each webhook payload contains:
* `timestamp`: ISO 8601 timestamp of when the event occurred
* `type`: Event type
* `data`: Event-specific payload. For the 'task\_run.status' event, it is the complete [Task Run object](/api-reference/tasks/retrieve-task-run)
### Example Payloads
```json Success theme={"system"}
{
"timestamp": "2025-04-23T20:21:48.037943Z",
"type": "task_run.status",
"data": {
"run_id": "trun_9907962f83aa4d9d98fd7f4bf745d654",
"status": "completed",
"is_active": false,
"warnings": null,
"error": null,
"processor": "core",
"metadata": {
"key": "value"
},
"created_at": "2025-04-23T20:21:48.037943Z",
"modified_at": "2025-04-23T20:21:48.037943Z"
}
}
```
```json Failure theme={"system"}
{
"timestamp": "2025-04-23T20:21:48.037943Z",
"type": "task_run.status",
"data": {
"run_id": "trun_9907962f83aa4d9d98fd7f4bf745d654",
"status": "failed",
"is_active": false,
"warnings": null,
"error": {
"message": "Task execution failed",
"details": "Additional error details"
},
"processor": "core",
"metadata": {
"key": "value"
},
"created_at": "2025-04-23T20:21:48.037943Z",
"modified_at": "2025-04-23T20:21:48.037943Z"
}
}
```
## Security & Verification
For information on HMAC signature verification, including code examples in multiple languages, see the [Webhook Setup Guide - Security & Verification](/resources/webhook-setup#security--verification) section.
## Retry Policy
See the [Webhook Setup Guide - Retry Policy](/resources/webhook-setup#retry-policy) for details on webhook delivery retry configuration.
## Best Practices
For webhook implementation best practices, including signature verification, handling duplicates, and async processing, see the [Webhook Setup Guide - Best Practices](/resources/webhook-setup#best-practices) section.