Writing Tilebox Workflows
Create or modify Python workflow source code and local/runtime behavior. Keep platform APIs in Tilebox references, portable science and IO in geospatial references, and end-to-end combinations in recipes.
Start With Current APIs And The Right Skill
When an API is unclear or remembered behavior may be stale, inspect the installed version or search current docs:
tilebox docs search "Task ExecutionContext submit_subtasks"
tilebox docs search "logging tracing context.logger context.tracer"
tilebox docs search "caches job_cache"
Delegate adjacent ownership:
tilebox-workflow-releases: project initialization, pyproject.toml scaffold, build, publish, deploy, and runner configuration.
tilebox-workflow-jobs: submit, wait, list, inspect, retry, cancel, and cluster operations.
tilebox-datasets: source/product recommendations, exact dataset slugs, provider credentials, auxiliary catalogues, and CLI inspection.
tilebox-cli: authentication, CLI discovery, JSON output, and docs search.
tilebox-workflow-automations: cron and storage-triggered automations.
For a new project, use tilebox workflow init through tilebox-workflow-releases; do not hand-scaffold project configuration. Authoring may edit the generated code after initialization.
Plan Before Coding
For outcome-oriented Earth observation work, read reference/geospatial/project-planning.md. Confirm resolution, temporal coverage, observation quality, comparability, required inputs, validation, and delivery. Use tilebox-datasets to select and inspect the source; do not duplicate its catalogue or credential guidance.
Sketch the task graph:
- Identify root, worker, and aggregation stages.
- Choose a fanout axis: scenes, products, AOIs, time windows, spatial chunks, or model tiles.
- Mark real barriers with
depends_on; avoid unnecessary chains.
- Choose task fields versus
context.job_cache versus durable object/Zarr artifacts.
- Choose retry behavior for idempotent network/storage operations.
Structure Non-Trivial Workflows As Packages
Treat the generated runner.py as a starting scaffold, not the default home for the entire implementation. A single file is acceptable for a genuinely small prototype with one or two short tasks and little processing logic. When the workflow has multiple stages or task types, substantial dataset/raster/encoding logic, reusable helpers, or independently testable processing logic, split it into an importable package with modules organized by coherent responsibility.
Keep runner.py as a thin composition root: import every registered task, construct the Runner, and configure runner-level cache or logging. Do not put task execution methods, dataset queries, raster processing, scientific algorithms, or output encoding there. Name the module containing the root task and main fanout after the workflow capability, such as rgb_timelapse.py, rather than a generic orchestration.py. Group other related task classes by workflow stage or domain, use concrete stage names such as aggregation.py, and keep portable processing/IO functions separate from Tilebox orchestration when that makes them easier to test. Avoid both a catch-all utils.py and one module per tiny class; extract a module only when it has a clear role.
A non-trivial timelapse workflow might use:
runner.py # task registration and runner configuration only
rgb_timelapse/
__init__.py
tasks/
__init__.py
rgb_timelapse.py # root workflow task and fanout
frames.py # frame worker task
aggregation.py # frame aggregation and encoding task
imagery.py # bounded reads, masking, and rendering
encoding.py # deterministic GIF encoding functions
models.py # shared typed values, only when needed
tests/
test_imagery.py
test_encoding.py
Adapt the depth to the workflow: a smaller project can use tasks.py, imagery.py, and runner.py without a tasks/ subpackage. Do not unit-test Tilebox task classes merely to assert submitted tasks, dependencies, progress, or logging; verify orchestration through release build/task discovery and a representative job's actual graph. Add focused tests only where useful for substantive underlying functions such as scene selection/grouping, transforms, masking, rendering, or encoding. Extract those functions from task execution methods so they can be tested without mocking ExecutionContext, but do not create trivial helpers solely to produce tests. Ensure every package module needed at runtime is included by the release build configuration, and verify imports from the built artifact rather than only from the working tree.
Require Task-Level Parallelism
Task-level fanout is a workflow design requirement, not an optional optimization. Whenever the requested output contains two or more independent units—such as scenes, seasonal or other time periods, AOIs, products, spatial chunks, or model tiles—represent those units as separate Tilebox tasks submitted with context.submit_subtask(s). Infer this decomposition from the outcome; never require the user to name submit_subtasks, specify task counts, or prescribe a DAG. For example, a 12-scene timelapse that combines all frames requires a graph such as 1 root + 12 scene workers + 1 encoder, not one task with a 12-iteration loop.
Use an orchestration task to submit independent workers and a separate aggregation or publication task with depends_on when the output combines their results. Never hide schedulable fanout inside a sequential loop, asyncio.gather, threads, or local multiprocessing in one task; additional runners can only parallelize separate Tilebox tasks. A small first run or scheduling overhead is not a reason to collapse independent work into one task. If the natural units are too fine-grained for individual scheduling, batch them into multiple balanced worker tasks rather than one task. Keep work in one task only when no two units can run independently because the work is genuinely indivisible or dependency-ordered, or when the user explicitly requires single-task execution.
Before coding, record the expected graph shape. After running a representative job, compare execution_stats.total_tasks and task_summaries with that shape. Progress units and child tracing spans are not tasks and do not prove fanout. If the graph does not match, fix the workflow before reporting completion. When runner capacity permits, use task timestamps or spans to verify that independent workers overlapped.
Read reference/tilebox/tasks-and-graphs.md for core task semantics, reference/tilebox/geospatial-task-graphs.md for geospatial stage mapping, and reference/tilebox/state-and-artifacts.md for boundaries.
Define Typed Tasks
Task applies dataclass behavior; fields are serialized inputs.
Use the simplest idiomatic type that expresses each field. Before inventing a representation, check whether Python, Tilebox, or the processing library already provides the value type. Do not recreate an established type as a custom dataclass, named tuple, or ad hoc tuple/list/dictionary shape merely to make it serializable. Ordinary containers remain appropriate when they are the natural Python shape, such as tuple[datetime, datetime] for a time range. Read reference/tilebox/tasks-and-graphs.md for supported types and examples.
from tilebox.workflows import ExecutionContext, Task
class ProcessScene(Task):
scene_id: str
@staticmethod
def identifier() -> tuple[str, str]:
return "tilebox.com/example/ProcessScene", "v1.0"
def execute(self, context: ExecutionContext) -> None:
context.current_task.display = f"ProcessScene({self.scene_id})"
context.logger.info("Processing scene", scene_id=self.scene_id)
- Default
v0.0 identifiers are acceptable for prototypes. Stable identifiers return (name, vX.Y).
- Minor versions are forward-compatible; bump major for breaking input/behavior changes.
- Keep the complete serialized input at most 2048 bytes. Pass compact plain-Python or supported library values, IDs, keys, and small configuration—not arrays, dataframes, xarray datasets, large geometry, manifests, credentials, clients, open files, or local paths. Use
context.job_cache or durable storage according to reference/tilebox/state-and-artifacts.md.
- Register every task class used by jobs with the runner.
Await Async IO Directly In Tasks
Define async def execute(self, context: ExecutionContext) -> None whenever a task uses async APIs, including tilebox.storage.aio, async HTTP clients, or bounded concurrent requests. Await those APIs directly; never wrap them in asyncio.run(...). The workflow executor awaits async execute methods while preserving normal failure, retry, tracing, and completion behavior.
import asyncio
from niquests import AsyncSession
class FetchMetadata(Task):
urls: list[str]
async def execute(self, context: ExecutionContext) -> None:
async with AsyncSession() as client:
responses = await asyncio.gather(
*(client.get(url) for url in self.urls)
)
for response in responses:
response.raise_for_status()
context.logger.info("Metadata fetched", count=len(responses))
Choose client lifetime according to its event-loop behavior: create niquests.AsyncSession inside execute, while tilebox.storage.aio.Client may be reused across task executions. See reference/tilebox/tasks-and-graphs.md for details. Bound large request sets with a semaphore or bounded batches. Async concurrency is appropriate for multiple IO requests that belong to one task, such as the bands or byte ranges needed for one scene. It does not replace task-level fanout for independent scenes, AOIs, time periods, products, chunks, or model tiles. Keep runner.run_all() and runner.run_forever() in synchronous runner entrypoints rather than calling them from async code.
Build Simple Dynamic Graphs
class ProcessScenes(Task):
scene_ids: list[str]
def execute(self, context: ExecutionContext) -> None:
context.current_task.display = f"ProcessScenes(n={len(self.scene_ids)})"
workers = context.submit_subtasks(
[ProcessScene(scene_id) for scene_id in self.scene_ids],
max_retries=3,
)
context.submit_subtask(PublishSummary(), depends_on=workers)
- Use
submit_subtask for one child and submit_subtasks for homogeneous batches.
- Pass returned handles to
depends_on; prefer stage barriers over thousands of unique pairwise dependencies.
- Use
optional=True only for non-critical work.
- Make side effects idempotent: deterministic keys, overwrite-safe writes, valid-output checks, or atomic commits. Retrying uses the original task input.
Progress And Observability
Set concise current_task.display labels before expensive work. For meaningful fanout, the submitting task calls context.progress(name).add(n) and each successful worker calls .done(1) after completing its represented unit. Totals and completions must match.
Use context.logger with structured fields and logger.bind for repeated context. In exception handlers, call logger.exception and re-raise. Wrap expensive IO/compute/publish phases in context.tracer.span(...) and add useful filter attributes. Configure console logging in the runner entrypoint, not task classes.
Dataset, Asset, And Storage Boundaries
- Query mechanics after source selection:
reference/tilebox/datasets-and-datapoints.md.
- Asset decoding and metadata:
reference/tilebox/assets.md.
- Asset byte access and bounded COG windows:
reference/tilebox/storage-access.md.
Do not reconstruct provider paths, repeat storage API snippets, or embed source/provider setup. Apply scale/offset, nodata, masks, alignment, and reprojection explicitly. Keep downloads in the consuming task and pass durable IDs/keys—not local paths—to another task.
Dependencies And Operations
Declare runtime dependencies in pyproject.toml, including optional libraries whose types appear in task fields. Tilebox loads their serializers lazily but does not install those libraries. Run uv sync, avoid editable/local-path dependencies, and see reference/tilebox/dependencies-and-packaging.md. Use authoring for source and focused local checks only. Use tilebox-workflow-releases for init/build/deploy and tilebox-workflow-jobs for submission/wait/clusters.
Reference Routing
Tilebox Platform
| Reference |
Use when |
reference/tilebox/tasks-and-graphs.md |
Defining/versioning tasks, selecting supported input types, respecting input limits, submitting dependencies, retries, progress, observability, registration, and runner modes. |
reference/tilebox/datasets-and-datapoints.md |
Querying an already-selected dataset, inspecting samples, selecting exactly one datapoint, or iterating datapoints. |
reference/tilebox/assets.md |
Decoding AssetCollection, semantic keys, raster metadata, scale/offset, or explicit overrides. |
reference/tilebox/storage-access.md |
Resolving/reading/streaming/downloading/opening Tilebox assets, access policy, anonymous access, windows, or concurrency. |
reference/tilebox/state-and-artifacts.md |
Choosing task inputs, job cache, object/Zarr artifacts, local scratch, and retry-safe keys. |
reference/tilebox/dependencies-and-packaging.md |
Declaring uv-compatible workflow dependencies and release-safe packages. |
reference/tilebox/geospatial-task-graphs.md |
Mapping scenes, windows, chunks, reductions, and progress to Tilebox tasks. |
Mixed Tilebox + Geospatial Recipes
| Reference |
Use when |
reference/tilebox/recipes/sentinel-2-cog.md |
Reading public L2A RGB plus SCL and aligning 20 m classes to 10 m RGB. |
reference/tilebox/recipes/sentinel-1-sar.md |
Building SAR change, flood, or maritime task graphs. |
reference/tilebox/recipes/time-series-composite.md |
Composing aligned scenes or producing timelapses. |
reference/tilebox/recipes/geospatial-ml.md |
Fanning out model tiles and aggregating geospatial predictions. |
Portable Geospatial
| Reference |
Use when |
reference/geospatial/project-planning.md |
Translating an outcome into feasible data, validation, and output requirements. |
reference/geospatial/raster-fundamentals.md |
Handling CRS, transform, shape, dtype, nodata, masks, scale, and units. |
reference/geospatial/io/cloud-native-raster.md |
Direct cloud-native GeoTIFF/COG reads outside canonical assets. |
reference/geospatial/io/object-storage.md |
Using obstore with S3, GCS, Azure, local, or compatible storage. |
reference/geospatial/io/zarr.md |
Designing Zarr schema, chunks, region writes, and labeled reads. |
reference/geospatial/io/cog-output.md |
Writing correct COG rasters and testing inputs or writer configurations when needed. |
reference/geospatial/io/formats-and-encoding.md |
Choosing COG/Zarr/NetCDF/GeoParquet and preserving essential encoding semantics. |
reference/geospatial/processing/grids-and-reprojection.md |
Choosing grids, reprojection, and semantic resampling. |
reference/geospatial/processing/masking-and-qa.md |
Handling nodata, masks, QA, classes, and morphology. |
reference/geospatial/processing/time-series.md |
Aligning, compositing, styling, and validating temporal imagery. |
reference/geospatial/processing/ml-inference.md |
Tiling, loading models, normalization, and prediction outputs. |
reference/geospatial/products/sentinel-1-sar.md |
Understanding SAR product choice, geometry, units, and caveats. |
reference/geospatial/products/sentinel-2-l2a.md |
Understanding L2A bands, resolution, scaling, and SCL. |
reference/geospatial/products/landsat-collection-2.md |
Understanding Landsat Level-2 bands, scaling, and QA bits. |
Verification Checklist
- All submitted tasks are registered and identifiers/versions agree.
- Task fields use idiomatic Python or existing library value types rather than structurally duplicating them in custom dataclasses or ad hoc containers.
- Every serialized task input is at most 2048 bytes.
- Cross-task data uses the right state/artifact boundary and deterministic keys.
- Retryable execution is re-entrant and input-compatible.
- High-fanout tasks have labels, useful progress, structured logs, and spans where warranted.
- Dependencies sync and task modules import in the intended environment.
- For scaffolded release projects,
tilebox workflow build-release --debug --json succeeds after editing generated task code.
- Non-trivial workflows use coherent package modules and keep the runner entrypoint limited to registration and runner-level configuration.
- Substantive underlying processing functions have focused tests where useful; Tilebox task orchestration is verified through build/task discovery and a representative job rather than task-class unit tests.
- Earth observation outputs have representative scientific validation, coverage/alignment checks, provenance, and stated limitations.
- A representative job's actual task summaries match the planned root, fanout, and aggregation stages; progress counters or child spans are not treated as task fanout.
1---2name: tilebox-workflow-authoring3description: Builds Python Tilebox workflows for Earth observation and geospatial processing, including satellite imagery queries, AOI and time-series pipelines, mosaics, spectral indices, classification, segmentation, change detection, and tiled ML or object detection. Use when implementing or modifying this processing, including when Tilebox is not named and no other execution platform is established.4license: MIT5---67# Writing Tilebox Workflows89Create or modify Python workflow source code and local/runtime behavior. Keep platform APIs in Tilebox references, portable science and IO in geospatial references, and end-to-end combinations in recipes.1011## Start With Current APIs And The Right Skill1213When an API is unclear or remembered behavior may be stale, inspect the installed version or search current docs:1415```bash16tilebox docs search "Task ExecutionContext submit_subtasks"17tilebox docs search "logging tracing context.logger context.tracer"18tilebox docs search "caches job_cache"19```2021Delegate adjacent ownership:2223- `tilebox-workflow-releases`: project initialization, `pyproject.toml` scaffold, build, publish, deploy, and runner configuration.24- `tilebox-workflow-jobs`: submit, wait, list, inspect, retry, cancel, and cluster operations.25- `tilebox-datasets`: source/product recommendations, exact dataset slugs, provider credentials, auxiliary catalogues, and CLI inspection.26- `tilebox-cli`: authentication, CLI discovery, JSON output, and docs search.27- `tilebox-workflow-automations`: cron and storage-triggered automations.2829For a new project, use `tilebox workflow init` through `tilebox-workflow-releases`; do not hand-scaffold project configuration. Authoring may edit the generated code after initialization.3031## Plan Before Coding3233For outcome-oriented Earth observation work, read `reference/geospatial/project-planning.md`. Confirm resolution, temporal coverage, observation quality, comparability, required inputs, validation, and delivery. Use `tilebox-datasets` to select and inspect the source; do not duplicate its catalogue or credential guidance.3435Sketch the task graph:36371. Identify root, worker, and aggregation stages.382. Choose a fanout axis: scenes, products, AOIs, time windows, spatial chunks, or model tiles.393. Mark real barriers with `depends_on`; avoid unnecessary chains.404. Choose task fields versus `context.job_cache` versus durable object/Zarr artifacts.415. Choose retry behavior for idempotent network/storage operations.4243### Structure Non-Trivial Workflows As Packages4445Treat the generated `runner.py` as a starting scaffold, not the default home for the entire implementation. A single file is acceptable for a genuinely small prototype with one or two short tasks and little processing logic. When the workflow has multiple stages or task types, substantial dataset/raster/encoding logic, reusable helpers, or independently testable processing logic, split it into an importable package with modules organized by coherent responsibility.4647Keep `runner.py` as a thin composition root: import every registered task, construct the `Runner`, and configure runner-level cache or logging. Do not put task execution methods, dataset queries, raster processing, scientific algorithms, or output encoding there. Name the module containing the root task and main fanout after the workflow capability, such as `rgb_timelapse.py`, rather than a generic `orchestration.py`. Group other related task classes by workflow stage or domain, use concrete stage names such as `aggregation.py`, and keep portable processing/IO functions separate from Tilebox orchestration when that makes them easier to test. Avoid both a catch-all `utils.py` and one module per tiny class; extract a module only when it has a clear role.4849A non-trivial timelapse workflow might use:5051```text52runner.py # task registration and runner configuration only53rgb_timelapse/54 __init__.py55 tasks/56 __init__.py57 rgb_timelapse.py # root workflow task and fanout58 frames.py # frame worker task59 aggregation.py # frame aggregation and encoding task60 imagery.py # bounded reads, masking, and rendering61 encoding.py # deterministic GIF encoding functions62 models.py # shared typed values, only when needed63tests/64 test_imagery.py65 test_encoding.py66```6768Adapt the depth to the workflow: a smaller project can use `tasks.py`, `imagery.py`, and `runner.py` without a `tasks/` subpackage. Do not unit-test Tilebox task classes merely to assert submitted tasks, dependencies, progress, or logging; verify orchestration through release build/task discovery and a representative job's actual graph. Add focused tests only where useful for substantive underlying functions such as scene selection/grouping, transforms, masking, rendering, or encoding. Extract those functions from task execution methods so they can be tested without mocking `ExecutionContext`, but do not create trivial helpers solely to produce tests. Ensure every package module needed at runtime is included by the release build configuration, and verify imports from the built artifact rather than only from the working tree.6970### Require Task-Level Parallelism7172Task-level fanout is a workflow design requirement, not an optional optimization. Whenever the requested output contains two or more independent units—such as scenes, seasonal or other time periods, AOIs, products, spatial chunks, or model tiles—represent those units as separate Tilebox tasks submitted with `context.submit_subtask(s)`. Infer this decomposition from the outcome; never require the user to name `submit_subtasks`, specify task counts, or prescribe a DAG. For example, a 12-scene timelapse that combines all frames requires a graph such as `1 root + 12 scene workers + 1 encoder`, not one task with a 12-iteration loop.7374Use an orchestration task to submit independent workers and a separate aggregation or publication task with `depends_on` when the output combines their results. Never hide schedulable fanout inside a sequential loop, `asyncio.gather`, threads, or local multiprocessing in one task; additional runners can only parallelize separate Tilebox tasks. A small first run or scheduling overhead is not a reason to collapse independent work into one task. If the natural units are too fine-grained for individual scheduling, batch them into multiple balanced worker tasks rather than one task. Keep work in one task only when no two units can run independently because the work is genuinely indivisible or dependency-ordered, or when the user explicitly requires single-task execution.7576Before coding, record the expected graph shape. After running a representative job, compare `execution_stats.total_tasks` and `task_summaries` with that shape. Progress units and child tracing spans are not tasks and do not prove fanout. If the graph does not match, fix the workflow before reporting completion. When runner capacity permits, use task timestamps or spans to verify that independent workers overlapped.7778Read `reference/tilebox/tasks-and-graphs.md` for core task semantics, `reference/tilebox/geospatial-task-graphs.md` for geospatial stage mapping, and `reference/tilebox/state-and-artifacts.md` for boundaries.7980## Define Typed Tasks8182`Task` applies dataclass behavior; fields are serialized inputs.8384Use the simplest idiomatic type that expresses each field. Before inventing a representation, check whether Python, Tilebox, or the processing library already provides the value type. Do not recreate an established type as a custom dataclass, named tuple, or ad hoc tuple/list/dictionary shape merely to make it serializable. Ordinary containers remain appropriate when they are the natural Python shape, such as `tuple[datetime, datetime]` for a time range. Read `reference/tilebox/tasks-and-graphs.md` for supported types and examples.8586```python87from tilebox.workflows import ExecutionContext, Task888990class ProcessScene(Task):91 scene_id: str9293 @staticmethod94 def identifier() -> tuple[str, str]:95 return "tilebox.com/example/ProcessScene", "v1.0"9697 def execute(self, context: ExecutionContext) -> None:98 context.current_task.display = f"ProcessScene({self.scene_id})"99 context.logger.info("Processing scene", scene_id=self.scene_id)100```101102- Default `v0.0` identifiers are acceptable for prototypes. Stable identifiers return `(name, vX.Y)`.103- Minor versions are forward-compatible; bump major for breaking input/behavior changes.104- Keep the complete serialized input at most 2048 bytes. Pass compact plain-Python or supported library values, IDs, keys, and small configuration—not arrays, dataframes, xarray datasets, large geometry, manifests, credentials, clients, open files, or local paths. Use `context.job_cache` or durable storage according to `reference/tilebox/state-and-artifacts.md`.105- Register every task class used by jobs with the runner.106107### Await Async IO Directly In Tasks108109Define `async def execute(self, context: ExecutionContext) -> None` whenever a task uses async APIs, including `tilebox.storage.aio`, async HTTP clients, or bounded concurrent requests. Await those APIs directly; never wrap them in `asyncio.run(...)`. The workflow executor awaits async `execute` methods while preserving normal failure, retry, tracing, and completion behavior.110111```python112import asyncio113114from niquests import AsyncSession115116117class FetchMetadata(Task):118 urls: list[str]119120 async def execute(self, context: ExecutionContext) -> None:121 async with AsyncSession() as client:122 responses = await asyncio.gather(123 *(client.get(url) for url in self.urls)124 )125 for response in responses:126 response.raise_for_status()127 context.logger.info("Metadata fetched", count=len(responses))128```129130Choose client lifetime according to its event-loop behavior: create `niquests.AsyncSession` inside `execute`, while `tilebox.storage.aio.Client` may be reused across task executions. See `reference/tilebox/tasks-and-graphs.md` for details. Bound large request sets with a semaphore or bounded batches. Async concurrency is appropriate for multiple IO requests that belong to one task, such as the bands or byte ranges needed for one scene. It does not replace task-level fanout for independent scenes, AOIs, time periods, products, chunks, or model tiles. Keep `runner.run_all()` and `runner.run_forever()` in synchronous runner entrypoints rather than calling them from async code.131132## Build Simple Dynamic Graphs133134```python135class ProcessScenes(Task):136 scene_ids: list[str]137138 def execute(self, context: ExecutionContext) -> None:139 context.current_task.display = f"ProcessScenes(n={len(self.scene_ids)})"140 workers = context.submit_subtasks(141 [ProcessScene(scene_id) for scene_id in self.scene_ids],142 max_retries=3,143 )144 context.submit_subtask(PublishSummary(), depends_on=workers)145```146147- Use `submit_subtask` for one child and `submit_subtasks` for homogeneous batches.148- Pass returned handles to `depends_on`; prefer stage barriers over thousands of unique pairwise dependencies.149- Use `optional=True` only for non-critical work.150- Make side effects idempotent: deterministic keys, overwrite-safe writes, valid-output checks, or atomic commits. Retrying uses the original task input.151152## Progress And Observability153154Set concise `current_task.display` labels before expensive work. For meaningful fanout, the submitting task calls `context.progress(name).add(n)` and each successful worker calls `.done(1)` after completing its represented unit. Totals and completions must match.155156Use `context.logger` with structured fields and `logger.bind` for repeated context. In exception handlers, call `logger.exception` and re-raise. Wrap expensive IO/compute/publish phases in `context.tracer.span(...)` and add useful filter attributes. Configure console logging in the runner entrypoint, not task classes.157158## Dataset, Asset, And Storage Boundaries159160- Query mechanics after source selection: `reference/tilebox/datasets-and-datapoints.md`.161- Asset decoding and metadata: `reference/tilebox/assets.md`.162- Asset byte access and bounded COG windows: `reference/tilebox/storage-access.md`.163164Do not reconstruct provider paths, repeat storage API snippets, or embed source/provider setup. Apply scale/offset, nodata, masks, alignment, and reprojection explicitly. Keep downloads in the consuming task and pass durable IDs/keys—not local paths—to another task.165166## Dependencies And Operations167168Declare runtime dependencies in `pyproject.toml`, including optional libraries whose types appear in task fields. Tilebox loads their serializers lazily but does not install those libraries. Run `uv sync`, avoid editable/local-path dependencies, and see `reference/tilebox/dependencies-and-packaging.md`. Use authoring for source and focused local checks only. Use `tilebox-workflow-releases` for init/build/deploy and `tilebox-workflow-jobs` for submission/wait/clusters.169170## Reference Routing171172### Tilebox Platform173174| Reference | Use when |175| --- | --- |176| `reference/tilebox/tasks-and-graphs.md` | Defining/versioning tasks, selecting supported input types, respecting input limits, submitting dependencies, retries, progress, observability, registration, and runner modes. |177| `reference/tilebox/datasets-and-datapoints.md` | Querying an already-selected dataset, inspecting samples, selecting exactly one datapoint, or iterating datapoints. |178| `reference/tilebox/assets.md` | Decoding `AssetCollection`, semantic keys, raster metadata, scale/offset, or explicit overrides. |179| `reference/tilebox/storage-access.md` | Resolving/reading/streaming/downloading/opening Tilebox assets, access policy, anonymous access, windows, or concurrency. |180| `reference/tilebox/state-and-artifacts.md` | Choosing task inputs, job cache, object/Zarr artifacts, local scratch, and retry-safe keys. |181| `reference/tilebox/dependencies-and-packaging.md` | Declaring uv-compatible workflow dependencies and release-safe packages. |182| `reference/tilebox/geospatial-task-graphs.md` | Mapping scenes, windows, chunks, reductions, and progress to Tilebox tasks. |183184### Mixed Tilebox + Geospatial Recipes185186| Reference | Use when |187| --- | --- |188| `reference/tilebox/recipes/sentinel-2-cog.md` | Reading public L2A RGB plus SCL and aligning 20 m classes to 10 m RGB. |189| `reference/tilebox/recipes/sentinel-1-sar.md` | Building SAR change, flood, or maritime task graphs. |190| `reference/tilebox/recipes/time-series-composite.md` | Composing aligned scenes or producing timelapses. |191| `reference/tilebox/recipes/geospatial-ml.md` | Fanning out model tiles and aggregating geospatial predictions. |192193### Portable Geospatial194195| Reference | Use when |196| --- | --- |197| `reference/geospatial/project-planning.md` | Translating an outcome into feasible data, validation, and output requirements. |198| `reference/geospatial/raster-fundamentals.md` | Handling CRS, transform, shape, dtype, nodata, masks, scale, and units. |199| `reference/geospatial/io/cloud-native-raster.md` | Direct cloud-native GeoTIFF/COG reads outside canonical assets. |200| `reference/geospatial/io/object-storage.md` | Using obstore with S3, GCS, Azure, local, or compatible storage. |201| `reference/geospatial/io/zarr.md` | Designing Zarr schema, chunks, region writes, and labeled reads. |202| `reference/geospatial/io/cog-output.md` | Writing correct COG rasters and testing inputs or writer configurations when needed. |203| `reference/geospatial/io/formats-and-encoding.md` | Choosing COG/Zarr/NetCDF/GeoParquet and preserving essential encoding semantics. |204| `reference/geospatial/processing/grids-and-reprojection.md` | Choosing grids, reprojection, and semantic resampling. |205| `reference/geospatial/processing/masking-and-qa.md` | Handling nodata, masks, QA, classes, and morphology. |206| `reference/geospatial/processing/time-series.md` | Aligning, compositing, styling, and validating temporal imagery. |207| `reference/geospatial/processing/ml-inference.md` | Tiling, loading models, normalization, and prediction outputs. |208| `reference/geospatial/products/sentinel-1-sar.md` | Understanding SAR product choice, geometry, units, and caveats. |209| `reference/geospatial/products/sentinel-2-l2a.md` | Understanding L2A bands, resolution, scaling, and SCL. |210| `reference/geospatial/products/landsat-collection-2.md` | Understanding Landsat Level-2 bands, scaling, and QA bits. |211212## Verification Checklist2132141. All submitted tasks are registered and identifiers/versions agree.2152. Task fields use idiomatic Python or existing library value types rather than structurally duplicating them in custom dataclasses or ad hoc containers.2163. Every serialized task input is at most 2048 bytes.2174. Cross-task data uses the right state/artifact boundary and deterministic keys.2185. Retryable execution is re-entrant and input-compatible.2196. High-fanout tasks have labels, useful progress, structured logs, and spans where warranted.2207. Dependencies sync and task modules import in the intended environment.2218. For scaffolded release projects, `tilebox workflow build-release --debug --json` succeeds after editing generated task code.2229. Non-trivial workflows use coherent package modules and keep the runner entrypoint limited to registration and runner-level configuration.22310. Substantive underlying processing functions have focused tests where useful; Tilebox task orchestration is verified through build/task discovery and a representative job rather than task-class unit tests.22411. Earth observation outputs have representative scientific validation, coverage/alignment checks, provenance, and stated limitations.22512. A representative job's actual task summaries match the planned root, fanout, and aggregation stages; progress counters or child spans are not treated as task fanout.