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

# Run batch jobs

> Fan work across autoscaled instances with map, keep expensive setup warm, and call the result from anywhere with your API key.

This guide is for developers with work that is too slow for one process: scraping
a list, embedding a corpus, converting files, or running a simulation sweep.

**Prerequisites:** `CYCLS_API_KEY`. Docker only if you want to test locally.

## 1. Write the unit of work

Write the function for one item. Parallelism is a call site decision, not a code
change.

```python scrape.py theme={null}
import cycls

image = cycls.Image().pip("httpx", "selectolax")


@cycls.function(image=image, concurrency=1)
def scrape(url: str):
    import httpx
    from selectolax.parser import HTMLParser

    try:
        r = httpx.get(url, timeout=30, follow_redirects=True)
        tree = HTMLParser(r.text)
        title = tree.css_first("title")
        return {"url": url, "status": r.status_code,
                "title": title.text() if title else None}
    except Exception as e:
        return {"url": url, "error": str(e)}
```

Returning the error as data matters. `map` raises on the first exception, so a
single bad URL would otherwise abort the batch.

`concurrency=1` gives each call its own instance, which is what you want for
CPU-bound work. Leave it high for I/O-bound work so one instance handles many
calls.

## 2. Test one item locally

```bash theme={null}
cycls run scrape.py --url https://example.com
# {'url': 'https://example.com', 'status': 200, 'title': 'Example Domain'}
```

## 3. Fan out

```python theme={null}
@cycls.local_entrypoint
def main():
    urls = [line.strip() for line in open("urls.txt") if line.strip()]
    results = scrape.map(urls)
    ok = [r for r in results if "error" not in r]
    print(f"{len(ok)} of {len(results)} succeeded")
```

```bash theme={null}
cycls run scrape.py
```

`map` runs one call per item across autoscaled instances and returns results in
input order. The entrypoint runs on your machine, so it can read local files and
print progress while the work happens in the cloud.

```python theme={null}
scrape.map(urls, workers=32)   # raise the fan-out width
```

## 4. Keep expensive setup warm

The process survives between calls on an instance, so a mutable default holds a
loaded model.

```python embed.py theme={null}
import cycls


@cycls.function(image=cycls.Image().pip("fastembed"), memory="4Gi")
def embed(texts, _model={}):
    if "m" not in _model:
        from fastembed import TextEmbedding
        _model["m"] = TextEmbedding("BAAI/bge-small-en-v1.5")
    return [v.tolist() for v in _model["m"].embed(list(texts))]
```

The first call on each instance loads the model. Every later call on that
instance skips the load. Batch the input so each call does real work:

```python theme={null}
@cycls.local_entrypoint
def main():
    docs = [line.strip() for line in open("corpus.txt")]
    batches = [docs[i:i + 256] for i in range(0, len(docs), 256)]
    vectors = [v for batch in embed.map(batches) for v in batch]
    print(len(vectors))
```

## 5. Write results where they persist

Return values travel over the wire, so keep them small. Large output belongs on a
[volume](/build/volumes).

```python theme={null}
lake = cycls.Volume("scrapes")


@cycls.function(image=image, volumes={"/out": lake})
def scrape(url: str):
    import hashlib, json, pathlib
    ...
    key = hashlib.sha256(url.encode()).hexdigest()[:16]
    pathlib.Path(f"/out/pages/{key}.json").write_text(json.dumps(record))
    return {"url": url, "key": key}
```

Pull them down later without writing an endpoint:

```bash theme={null}
cycls volume ls scrapes pages/
cycls volume get scrapes pages/6f1a2b3c4d5e6f70.json .
```

## 6. Deploy it as a named endpoint

```bash theme={null}
cycls deploy scrape.py
# Deployed: https://scrape.cycls.ai
# Call it: cycls.remote("scrape")(...)
```

```python theme={null}
import cycls

one = cycls.remote("scrape")("https://example.com")
many = cycls.remote("scrape").map(urls)
```

Any machine with your `CYCLS_API_KEY` can call it, including a laptop with no
source and no Docker. An agent can call it from a
[tool handler](/guides/custom-tool), which keeps heavy dependencies out of the
chat container.

```python theme={null}
import asyncio

read = cycls.remote("scrape")


async def read_pages(args):
    return await asyncio.to_thread(read.map, args["urls"])


llm = cycls.LLM().tools(TOOLS).on("read_pages", read_pages)
```

## Sizing

| Workload                      | Settings                                                     |
| ----------------------------- | ------------------------------------------------------------ |
| CPU-bound, one item at a time | `concurrency=1`, `cpu=2` or higher                           |
| I/O-bound, many small calls   | leave `concurrency` high, `cpu=1`                            |
| Large model in memory         | `memory="8Gi"` or more, warm the model in a default argument |
| Long single run               | raise `timeout`, up to 3600 seconds                          |

## Limits

* Payloads should stay well under 30MB per call. Use a volume for anything larger.
* A call times out after one hour. Split longer work or checkpoint it.
* If the executor is replaced mid-fan, the whole fan retries, so side effects
  should be idempotent.
* Tracebacks from remote code have correct file names and line numbers but no
  source lines, because the container holds bytecode rather than your files.

## Next

<Card title="Scheduled reports" icon="clock" href="/guides/scheduled-reports">
  Run the same work nightly and leave the output somewhere durable.
</Card>
