> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openreward.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Backdated Web Tools

> Give agents web search and fetch bounded to a cutoff date, with no post-cutoff leakage

## Introduction

`BackSearchToolset` gives an agent **web search and page fetch bounded to a
cutoff date** — "the web as it existed on or before *date X*". Every request
carries an `as_of` date and the backing corpus only returns documents published
on or before it. This is what makes the tools leakage-safe for evaluation and
prediction tasks: an agent researching a question dated to the past never sees
anything published after the cutoff.

There are two ways to use it:

* **As an environment toolset** — declare `BackSearchToolset` and the agent gets
  `web_search` / `web_fetch` tools.
* **Standalone** — call `Backsearch` / `Backfetch` directly from any Python or
  agent loop, no environment required.

<Note>
  Requires `openreward >= 0.1.134`. The web tools authenticate with your standard
  `OPENREWARD_API_KEY` — no separate key is needed.
</Note>

## Configuration

The only required setting is your OpenReward API key (the same key the rest of
the SDK uses). In a hosted environment it is forwarded automatically as a
session secret.

```bash theme={null}
export OPENREWARD_API_KEY="or_xxxxxxxx"
```

| Variable                                                                                    | Required | Purpose                                                                            |
| ------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- |
| `OPENREWARD_API_KEY`                                                                        | **yes**  | Your OpenReward API key (the web service uses the same key)                        |
| `OPENREWARD_WEB_AS_OF`                                                                      | no       | Default cutoff date (ISO `YYYY-MM-DD`); a per-call or per-env `as_of` overrides it |
| `OPENREWARD_WEB_SEARCH_URL` / `OPENREWARD_WEB_FETCH_URL` / `OPENREWARD_WEB_DOMAIN_INFO_URL` | no       | Override backend endpoints (default: `https://search.openreward.ai/*`)             |
| `OPENREWARD_WEB_SKIP_PREFLIGHT`                                                             | no       | Set to `1` to skip the domain-policy preflight                                     |
| `OPENREWARD_WEB_PREAPPROVED_HOSTS`                                                          | no       | Comma-separated hosts whose fetched content is returned verbatim                   |

<Note>
  Search runs over a fixed historical corpus. Choose `as_of` dates inside the
  covered window — dates outside it return no results. Confirm the current
  coverage with the OpenReward team.
</Note>

## Using the toolset in an environment

Declare `BackSearchToolset` at class level. Unlike the [harness
toolsets](/harnesses/harness-toolsets), it is **not** sandbox-backed — it talks
to the web service over HTTP — so your environment does not need a `self.sandbox`.

```python theme={null}
from openreward.environments import Environment, Split, TextBlock
from openreward.toolsets import BackSearchToolset

class ResearchEnv(Environment):
    toolsets = [BackSearchToolset]

    # Optional cutoff. May be a plain attribute, a @property, or a zero-arg
    # callable — it is read live on every tool call (see "Progressing the
    # cutoff" below).
    web_as_of = "2025-12-01"

    @classmethod
    def list_splits(cls):
        return [Split(name="test", type="test")]

    @classmethod
    def list_tasks(cls, split):
        return [{"id": "1", "question": "What happened with X in late 2025?"}]

    def get_prompt(self):
        return [TextBlock(text="Research the question using web_search and web_fetch, then answer with sources.")]
```

The agent then sees two tools:

| Tool         | Input                                                                                 | Output                                                                            |
| ------------ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `web_search` | `query` (≥ 2 chars), optional `allowed_domains` **or** `blocked_domains` (never both) | A `Links:` list of `{title, url}` sources plus a reminder to cite them            |
| `web_fetch`  | `url`, `prompt`                                                                       | Extracted page text (or a `REDIRECT DETECTED` notice to re-call with the new URL) |

### Selecting it by name in a session

`BackSearchToolset` is a registered built-in, so a client can also attach it to
a session by name:

```python theme={null}
from openreward import OpenReward

or_client = OpenReward()
env = or_client.environments.get(name="MyOrg/ResearchEnv")
tasks = env.list_tasks(split="test")

with env.session(task=tasks[0], toolset="backsearch") as session:
    result = session.call_tool("web_search", {"query": "US interest rate decision December 2025"})
    print(result.blocks[0].text)
```

### Composing with other toolsets

Because it needs no sandbox, `BackSearchToolset` composes cleanly with
sandbox-backed toolsets — e.g. web research plus a file workspace:

```python theme={null}
from openreward.toolsets import BackSearchToolset, CLIToolset

class ResearchAndCodeEnv(Environment):
    toolsets = [BackSearchToolset, CLIToolset]
    # ... CLIToolset needs self.sandbox; BackSearchToolset does not ...
```

All tools from both toolsets are exposed, and the framework rejects duplicate
tool names. `BackSearchToolset` uses `web_search` / `web_fetch`, which do not
collide with the file/CLI toolsets.

## Progressing the cutoff

The cutoff is resolved **on every tool call**, in priority order:

1. an explicit `as_of=` passed to the toolset constructor (a static pin), else
2. `env.web_as_of`, else
3. the `OPENREWARD_WEB_AS_OF` environment variable, else
4. the backend default (today).

Because `env.web_as_of` is read live — not snapshotted when the toolset is
built — an environment whose simulated clock advances stays correctly bounded to
its current day. Expose it as a `@property` or a callable:

```python theme={null}
class TimeSteppedEnv(Environment):
    toolsets = [BackSearchToolset]

    @property
    def web_as_of(self) -> str:
        return self.sim_clock.today().isoformat()   # advances as the task steps
```

This matters because the framework builds and caches one toolset instance per
environment for the whole session — a construction-time snapshot would freeze
the date at the first call.

## Standalone usage

The same backdated search and fetch are available without an environment:

```python theme={null}
import asyncio
from openreward.tools import Backsearch, Backfetch

async def main():
    bs = Backsearch(as_of="2025-12-01")
    result = await bs.run("US interest rate decision December 2025")
    if result.ok:
        print(result.output)                 # citation-ready text
        for hit in result.data["hits"]:
            print(hit["title"], "->", hit["url"])

    bf = Backfetch(as_of="2025-12-01")
    page = await bf.run("https://example.com/article", "Summarise the key points")
    if page.ok:
        print(page.data["content"])

asyncio.run(main())
```

Both `run(...)` calls return a `WebToolResult`:

| Field                            | Meaning                                                                             |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| `.ok`                            | `True` on success, `False` on a recoverable error                                   |
| `.output`                        | The string to show a model (the result, or an error message)                        |
| `.data`                          | Structured payload — search: `{query, hits, mode}`; fetch: `{url, prompt, content}` |
| `.error_code` / `.error_message` | Set when `.ok` is `False` (e.g. `not-configured`, `invalid-url`, `domain-blocked`)  |

`Backsearch.run` accepts `allowed_domains` / `blocked_domains` and a per-call
`as_of`; `Backfetch.run` takes a `url`, a `prompt`, and a per-call `as_of`.

## Notes

* **Citations.** Search output is formatted so the model is reminded to cite the
  returned URLs as markdown links in its final answer.
* **Caching.** Fetches are cached in-process for 15 minutes, keyed by
  `(cutoff, url)`, so repeated fetches at the same cutoff are fast and never mix
  content across dates.
* **Soft errors.** Bad inputs (unparseable URL, blocked domain, missing key)
  come back as a normal result with `.ok == False` and a clear message rather
  than as an exception, so an agent can read it and recover.

## Next Steps

<CardGroup cols={2}>
  <Card title="Using Toolsets" icon="toolbox" href="/environments/using-toolsets">
    Compose document toolsets and build custom ones
  </Card>

  <Card title="Harness Toolsets" icon="wrench" href="/harnesses/harness-toolsets">
    Expose agent-native CLI tool surfaces backed by a sandbox
  </Card>

  <Card title="Building Agentic Environments" icon="robot" href="/environments/building-agentic-environments">
    Create sandbox-based environments from scratch
  </Card>

  <Card title="Keeping Secrets Secret" icon="lock" href="/environments/keeping-secrets-secret">
    How API keys and secrets reach your environment
  </Card>
</CardGroup>
