Simplify the EarthCODE workflow example¶
The EarthCODE tutorial's Create new workflow record section builds a nested dictionary in create_workflow_collection(). With this package, you can create an OGCRecord, assign common metadata through named accessors, and use ordinary PySTAC links. You no longer need a helper whose responsibility is assembling the record's JSON envelope.
This tutorial uses illustrative workflow metadata and uses OSC extension accessors for the source example's project and status metadata. It also identifies the differences between preserving that example and claiming OGC conformance.
1. Install and create the record¶
Install the package, then run the following blocks in order. Creating the record needs no catalog checkout or network access.
from datetime import datetime, timezone
import pystac
from pystac.extensions.ogc_record import OGCRecord
from pystac.extensions.osc import ItemOscExtension, OscStatus
from pystac.utils import datetime_to_str
project_id = "crop-mapping"
workflow = OGCRecord(
id="crop-mapping-workflow",
properties={"version": "1"},
)
osc = ItemOscExtension(workflow)
osc.project = project_id
osc.status = OscStatus.COMPLETED
workflow.type = "workflow"
workflow.title = "Crop mapping workflow"
workflow.description = "A reproducible workflow for mapping cropland."
workflow.license = "proprietary"
workflow.keywords = ["cropland", "classification"]
workflow.formats = [{"name": "openEO process graph"}]
workflow.created = workflow.updated = datetime_to_str(datetime.now(timezone.utc))
workflow.add_links([
pystac.Link(
rel="root", target="../../catalog.json",
media_type="application/json", title="Open Science Catalog",
),
pystac.Link(
rel="parent", target="../catalog.json",
media_type="application/json", title="Workflows",
),
pystac.Link(
rel="related", target=f"../../projects/{project_id}/collection.json",
media_type="application/json", title="Project: Crop mapping",
),
])
The adapter supplies the Feature envelope and null geometry. Common properties use accessors such as title, formats, and license; OSC fields use osc.project and osc.status. Only the custom version field remains explicit in properties. pystac.Link handles link serialization, including mapping target to JSON href and media_type to JSON type.
ItemOscExtension(workflow) binds the existing OSC property accessors directly to the Item-compatible record. Unlike OscExtension.ext(..., add_if_missing=True), this constructor does not add a STAC schema declaration. That matters here: the OSC STAC schema describes projects and products, not workflow records. This is accessor reuse, not a claim that the workflow validates against that schema.
The timestamp is computed once using timezone-aware UTC. These metadata accessors store strings, so datetime_to_str() supplies the serialized timestamp. created and updated describe metadata timestamps; they do not supply a STAC Item datetime.
2. See what the API replaces¶
| Responsibility | Dictionary-based helper | Implemented API |
|---|---|---|
| Feature envelope | Explicit type, geometry, properties, and links members |
OGCRecord(id=..., properties=...) and to_record_dict() |
| Common metadata | Nested property-key assignments | workflow.title, workflow.keywords, workflow.license, etc. |
| OSC metadata | Prefixed property-key assignments | osc.project and osc.status = OscStatus.COMPLETED |
| Resource links | Handwritten link dictionaries | pystac.Link and add_links() |
| Optional metadata removal | Delete a nested key | Assign None to a common metadata accessor |
| Independent copy | Copy the document and manage nested state | workflow.clone() |
| Load existing JSON | Work directly with nested dictionaries | OGCRecord.from_dict(document) |
The main benefit is less document-assembly code and one consistent object API for creation, editing, copying, and serialization. Domain decisions remain yours: which project the workflow belongs to, which formats it uses, and where its links point.
3. Inspect and round-trip the result¶
document = workflow.to_record_dict(transform_hrefs=False)
assert document["type"] == "Feature"
assert document["geometry"] is None
assert document["properties"]["title"] == workflow.title
assert document["properties"]["osc:project"] == project_id
assert document["properties"]["osc:status"] == OscStatus.COMPLETED.value
assert "osc:type" not in document["properties"]
assert "stac_extensions" not in document
assert "datetime" not in document["properties"]
assert "stac_version" not in document
assert document["links"][2]["href"] == (
f"../../projects/{project_id}/collection.json"
)
restored = OGCRecord.from_dict(document)
assert restored.title == workflow.title
restored_osc = ItemOscExtension(restored)
assert restored_osc.project == project_id
assert restored_osc.status == OscStatus.COMPLETED
assert restored.to_record_dict(transform_hrefs=False) == document
working_copy = restored.clone()
working_copy.keywords = None
assert "keywords" not in working_copy.properties
assert restored.keywords == ["cropland", "classification"]
transform_hrefs=False preserves the link strings for this standalone example. Relative links are intended for a record at workflows/<workflow-id>/record.json inside the catalog checkout. Moving the JSON to another directory changes what those links resolve to.
4. Write a staging file¶
This block writes workflow_record.json in the current directory, replacing it if it already exists:
import json
from pathlib import Path
output_path = Path("workflow_record.json")
output_path.write_text(
json.dumps(workflow.to_record_dict(transform_hrefs=False), indent=2) + "\n",
encoding="utf-8",
)
loaded = OGCRecord.from_dict(json.loads(output_path.read_text(encoding="utf-8")))
assert loaded.title == workflow.title
This is a staging artifact. Place it at the intended catalog location, add the catalog/project return links required by the contribution workflow, and run the catalog's validation before opening a pull request. The adapter does not perform those repository operations.
Compatibility choices¶
This example deliberately makes the following choices explicit:
- Resource type:
workflow.type = "workflow"addsproperties.type; the top-leveltyperemainsFeature. The unsupportedosc:type="workflow"assignment is omitted:OscTypeonly accepts project and product. Theosc:statusandversionvalues retain the EarthCODE example's additional metadata; workflow status is not required by the upstream OSC workflow convention. They are not evidence that the record satisfies the OSC STAC schema. - OSC scope: upstream OSC describes workflows with
osc:projectand arelatedproject link. Addchildlinks when experiments exist.OscTypeonly contains project and product; do not useOscExtension.apply_project()orapply_product()to construct a workflow, or declare their STAC schema for this record. - Conformance: the source example includes a record-core
/req/identifier. This tutorial omitsconformsTobecause constructing an object does not establish conformance. After verification, an applicable record-core conformance identifier ishttp://www.opengis.net/spec/ogcapi-records-1/1.0/conf/record-core, supplied throughconforms_toorworkflow.conforms_to. Conformance identifiers andstac_extensionsserve different purposes. - Optional members:
linkTemplatesis omitted when unused. Assignworkflow.link_templates = []if a consumer expects an explicit empty list. - Extent: the source helper accepts
workflow_extentwithout using it. No equivalent unused argument is needed here. If the resource has spatial or temporal coverage, supply appropriate recordgeometry,bbox, or OGCtimeexplicitly; a STAC Collection extent is not converted automatically.
These are documented differences, so the example is not a byte-for-byte replacement. See the OSC specification and OGC Record Core requirements for the contracts.
Neither setters nor serialization perform schema validation. workflow.validate() requires an explicit OGC validator; the catalog's additional contribution checks still apply. Continue with the OGC Record reference and workflow/experiment guide.