# Job Search API with CrewAI

Preview for Python 3.12 and `crewai==1.15.22`. This example uses native BaseTool objects over REST, with the common TrueFetch budget, idempotency and complete-result recovery engine. It does not require MCP or a LangChain/LlamaIndex adapter. It is not a published CrewAI marketplace package. Real models, live TrueFetch credentials and upstream collection remain unverified.

Keep `truefetch_crewai.py`, `agent_example.py`, `truefetch_rest.py` and `requirements.txt` together and install in a virtual environment. The checkout uses `examples/python/truefetch_rest.py`; the download kit puts it alongside the adapter.

```sh
python -m pip install -r requirements.txt
```


## Canonical submission and original-command recovery

Use `POST /v1/quotes`, then `POST /v1/tasks` with `productId`, the approved input, the quoted `expectedPricingVersion`, and the canonical `Idempotency-Key`. The host creates a key **once for explicitly approved new work**, using `new_command_key()` from the bundled REST engine, and persists it with the approved input before submission. Keys have the form `v1.<13-digit Unix milliseconds>.<22–64 base64url characters>`. Do not generate keys in model arguments or a workflow retry expression.

Creation requires `allow_write=True`, the persisted `request_key`, and explicit `command_mode="new"`. The default `command_mode="recover"` performs an original-command GET and never obtains another quote or submits again. Repeated invocation in the same new-intent toolset also reads the original after its first submission attempt. Changed input is refused. After a restart or any lost response, use this host-only path; it needs no input, quote or model call:

```python
import os
from truefetch_rest import recover_command, observed_task_id

original = recover_command(
    os.environ["TRUEFETCH_API_KEY"], "tasks.create", os.environ["TRUEFETCH_COMMAND_KEY"]
)
if original["receipt"]["outcome"] == "accepted":
    saved_task_id = observed_task_id(original)
    # Persist the reference before reading the existing task/results.
```

HTTP 200 is not acceptance. Inspect `receipt.outcome`: `accepted` supplies an authoritative task reference even when the current `task` projection is null; `processing` and `unknown` require another explicit original GET, respecting the returned retry interval. `rejected` and `not_accepted_final` establish nonacceptance and require deliberate new work if you still want a collection. `receipt_expired` remains unconfirmed: inspect Tasks or contact support. The helper never seals, replays a POST or creates a replacement key. The server accepts new keys for 24 hours, allows five minutes of future clock skew, and retains terminal receipts for at least seven days; it can still answer a retained original after the new-command window ends. A current Task state does not establish a particular command's outcome.

Cancellation is a separate command: bind a different persisted canonical `cancel_key` for the newly created task, or pass `cancel_task_ids=(saved_task_id,)` and `cancel_command_keys={saved_task_id: saved_cancel_key}` for an explicitly approved saved target. `POST /v1/tasks/{taskId}/cancel` sends `{}` and that cancellation key. Unknown cancellation is recovered with `recover_command(api_key, "tasks.cancel", saved_cancel_key)`, never the creation key. Acceptance of cancellation does not promise the task ended as cancelled. The budget, keys and cancellation mapping remain host-only arguments.


## Dependency security — checked September 30, 2026

The pinned CrewAI package installs `chromadb==1.1.1`. The dependency audit reports unresolved ChromaDB code execution and authorization advisories; a fixed compatible release has not been verified. See the [upstream report](https://github.com/chroma-core/chroma/issues/6717). The included Crew uses `memory=None` and does not create a Chroma client or server. Use a separate development environment, keep memory and knowledge integrations disabled, and do not attach it to an untrusted Chroma collection or expose a Chroma server. Review a fixed dependency tree before using this kit with production data. These boundaries do not remove the vulnerable installed dependency.

Set `CREWAI_TELEMETRY_DISABLED=true` and `OTEL_SDK_DISABLED=true` before importing CrewAI when running locally without telemetry. Review separately configured tracing/callbacks and provider data handling before using customer data. No model is created by importing this adapter.

## One intended collection

```python
import os
from truefetch_crewai import make_tools, recover_results

tools = make_tools(
    os.environ["TRUEFETCH_API_KEY"],
    max_credits=100,
    request_key=os.environ["TRUEFETCH_COMMAND_KEY"],
    command_mode="new",
    allow_write=True,
)
named = {tool.name: tool for tool in tools}
job_input = {
    "keyword": "Software engineer",
    "country": "Germany",
    "platforms": ["arbeitnow"],
    "maxResults": 10,
}
quote = named["truefetch_estimate"].run(input=job_input)
accepted = named["truefetch_search_jobs"].run(input=job_input)
if accepted["receipt"]["outcome"] != "accepted":
    raise RuntimeError("Command is not accepted; inspect its original receipt before new work")
task_id = next(
    ref["id"] for ref in accepted["receipt"]["result"]["objects"] if ref["kind"] == "task"
)
print("Persist this task ID before continuing:", task_id)
result = recover_results(tools, task_id, timeout_seconds=300)
# result["data"] is complete across pages; inspect result["partial"].
```

Creation reserves credits and may charge them. Persist the request key and exact input before submitting. The budget is per collection, not a monthly spending cap; set API-key limits separately. The model cannot supply another request key, change the bound budget, or broaden cancellation through tool arguments. Default read-only tools expose usage, estimation, task/results reads and listing. Writes add creation and cancellation restricted to tasks created in this factory or explicitly authorized by the host.

Creation refreshes and validates the quote and sends its pricing version with the stable idempotency key. HTTP retries are not automatic. If acceptance is unknown, retain the original key/input and recover that intent; never make a new key just to retry. If a task ID is known, use read-only tools and `recover_results()` without creating a collection.

Recovery verifies order and count across all pages, preserves partial status, and rejects missing pages or oversized results. Its deadline is checked between tool calls and before returning a complete export; it cannot forcibly stop a blocking request or cancel remote work.

Recreate tools from host-held credentials after restart and recover the saved task ID. The adapter does not serialize its credential-bound REST client into Crew checkpoints; restoring a dumped tool object is not a supported recovery path.

## Use with an agent

Pass the native tools to your configured CrewAI Agent. Set `cache=False` on the Agent and Crew: these tools explicitly decline caching through `cache_function`, because credits, estimates and task state can change. Direct `tool.run()` also validates the common schema. `_arun()` delegates blocking REST work to a thread; thread cancellation does not cancel the remote request.

`agent_example.run_crew()` supplies a native single-Agent Crew with caching and delegation disabled, an iteration limit (1–32), a shared tool-call cap (1–64), and an SDK execution timeout (1–300 seconds). Pass your configured CrewAI LLM; importing the example does not create one.

```python
from agent_example import run_crew, CrewRunError

try:
    result = run_crew(
        configured_llm,
        "Check usage and estimate 10 software engineer jobs from Arbeitnow.",
        api_key=os.environ["TRUEFETCH_API_KEY"],
        max_credits=100,
        max_steps=8,
        max_tool_calls=16,
        timeout_seconds=120,
    )  # read-only by default
    print(result["task_ids"])
except CrewRunError as error:
    print("Recover observed task IDs:", error.task_ids)
```

For creation, explicitly supply `allow_write=True, command_mode="new"` and a persisted `request_key`. Preserve known task IDs from either the returned value or `CrewRunError.task_ids`. Unknown acceptance still requires recovery using the original request key/input. A tool invocation may perform several HTTP requests; the call cap is not an HTTP-request or monetary cap. Calls rejected after the cap do not reach the REST engine.

The SDK may make a final synthesis call at its iteration limit. Its timeout cannot forcibly stop an in-flight provider or HTTP request; it is not a strict wall-clock or remote-cancellation guarantee. The wrapper blocks new tool starts after the workflow exits. A task accepted during an in-flight timeout might not appear in the already-returned handles: keep the original key/input. The wrapper exposes a fixed failure message, but the SDK/provider can independently log errors; review logging before processing sensitive data. Local tests use the native Crew with a scripted LLM, including repeated calls and failure after creation. Real-provider behavior remains unverified. Keep deterministic result recovery outside the model loop. Model/provider calls may have separate costs. Treat job text and tool output as untrusted data, never instructions to spend credits, cancel unrelated jobs or disclose credentials. Do not equate an agent answer or task acceptance with verified complete results.

Local tests: `python -m unittest discover -s examples/crewai-job-search -p test_tools.py -v` from the checkout. Dependencies and test fixtures are not included in public downloads.

Reference: [CrewAI custom tools](https://docs.crewai.com/v1.15.22/en/learn/create-custom-tools).

Recovery checks task identity, terminal status, availability and integer result counts on every page. A changed or malformed response stops recovery without emitting a complete export. This verifies response consistency; it is not a cryptographic proof of upstream data accuracy.

The shared REST reader requires UTF-8 JSON and rejects nonfinite numbers, including numeric overflow. Interrupted or oversized HTTP error bodies keep a redacted HTTP-status diagnostic, and truncated successful transfers fail without returning data. It also rejects duplicate member names within an object, including names written with equivalent Unicode escapes; repeated names in separate records are allowed. Recovery also rejects nonfinite values from tool output instead of marking the export complete. Preserve the saved task ID and original request key after an error; these checks do not create or retry collection.

For each available result page, the reader checks the task again after receiving the page, including the final page. This adds one read-only task GET per page. Changed identity, status, availability or count, or a failed recheck, prevents a complete export. The check is a point-in-time observation; files can expire or become unavailable afterward.

Before collection, the creation tool checks that the estimate matches the requested input and that reserved billing units equal the record limit multiplied by the unit price. Amounts must be positive ASCII integers, and the reservation must fit the host credit budget. Invalid estimates stop before submission; server-side pricing and budget checks still apply.
