How to create a workflow
Declare a workflow as structure, publish runs from your own code, and trace each forecast back to a workflow version.
A workflow is structure, not execution. It declares what a recurring computation produces — which timeseries on which objects — and points to the code that produces it. The code itself lives wherever you run it; when it finishes, it publishes a run to the platform with the output points inline. The platform stamps every point with the run's knowledge_time and records which workflow version it was published under — full traceability, end to end.
POST /platform/v3/workflows— declare a workflow.PATCH /platform/v3/workflows/{workflowId}— update it (creates a new version).POST /platform/v3/workflows/{workflowId}/runs— publish a run with its output points.GET /platform/v3/workflows/{workflowId}/runs— list runs.GET /platform/v3/workflows/{workflowId}/versions— list versions.
Declare a workflow
The definition carries three things: a source pointing at the external code, a declarative graph of the steps (for display in the UI — the platform doesn't execute it), and outputs naming the timeseries the runs will write. The schedule is for monitoring, not execution — external: true says runs are published from outside, and expected_cadence lets the platform flag when one doesn't arrive on time.
import requests
url = "https://api.rebaseenergy.dev/platform/v3/workflows"
headers = {"Authorization": "Bearer <your_api_key>"}
body = {
"name": "Storrun power forecast",
"description": "6-hourly power forecast for Storrun Wind Farm.",
"schedule": {"external": True, "expected_cadence": "0 */6 * * *"},
"definition": {
"source": {
"repository": "github.com/rebase/storrun-forecast",
"ref": "9f31c2a",
},
"graph": {
"nodes": [
{"id": "weather", "label": "Fetch weather", "type": "source"},
{"id": "power-model", "label": "Power model", "type": "model"},
{"id": "publish", "label": "Publish forecast", "type": "output"},
],
"edges": [
{"from": "weather", "to": "power-model"},
{"from": "power-model", "to": "publish"},
],
},
"outputs": [
{
"target_type": "site",
"target_id": "4dbb3433-402f-4276-8a55-7f27753f70dc",
"timeseries": "power-forecast",
},
{
"target_type": "site",
"target_id": "4dbb3433-402f-4276-8a55-7f27753f70dc",
"timeseries": "iceloss-forecast",
},
],
},
}
response = requests.post(url, headers=headers, json=body)
print(response.json()){
"workflow_id": "0f3c9a55-1e22-4d6b-9aa1-7e8b2c4d5f60",
"version_number": 1,
"is_current": true,
"name": "Storrun power forecast",
"schedule": {"external": true, "expected_cadence": "0 */6 * * *"},
"definition": {"source": {"...": "..."}, "graph": {"...": "..."}, "outputs": ["..."]},
"last_run_at": null
}The graph is purely descriptive — the code in source is the source of truth for what actually runs. It exists so the UI can render the pipeline (weather → model → publish) and so a reviewer can see the shape of a computation without reading the repo. (org_id and created_by are derived from the API key, so they don't appear in the request body.)
Publish a run
When your code has produced a forecast, publish it in one call: run metadata plus the points for each declared output. The run-level knowledge_time applies to every point — it is when this output came into existence.
import requests
workflow_id = "0f3c9a55-1e22-4d6b-9aa1-7e8b2c4d5f60"
url = f"https://api.rebaseenergy.dev/platform/v3/workflows/{workflow_id}/runs"
headers = {"Authorization": "Bearer <your_api_key>"}
body = {
"knowledge_time": "2026-06-03T06:00:00Z",
"status": "succeeded",
"outputs": [
{
"timeseries": "power-forecast",
"points": [
{"valid_time": "2026-06-03T07:00:00Z", "q10": 4200, "q50": 5100, "q90": 6300},
{"valid_time": "2026-06-03T08:00:00Z", "q10": 4000, "q50": 4900, "q90": 6100},
],
},
{
"timeseries": "iceloss-forecast",
"points": [
{"valid_time": "2026-06-03T07:00:00Z", "value": 0.08},
],
},
],
}
response = requests.post(url, headers=headers, json=body)
print(response.json())Each outputs entry references a declared output by its timeseries name — the target object comes from the workflow's definition. Points are columnar: every key other than valid_time is one of the series' declared columns. So for power-forecast (columns: ["q10", "q50", "q90"]), one row carries the whole quantile fan instead of one row per quantile. A series with a single value column, like iceloss-forecast, just uses value. Points don't carry their own knowledge_time; the platform stamps the run's value onto all of them, along with the run_id and the workflow version that was current at publish time.
{
"id": "b21f0c4e-7d18-4a90-bb3e-2c5a9f0e1d77",
"workflow_id": "0f3c9a55-1e22-4d6b-9aa1-7e8b2c4d5f60",
"workflow_version": 1,
"status": "succeeded",
"knowledge_time": "2026-06-03T06:00:00Z",
"published_at": "2026-06-03T06:04:12Z",
"outputs": [
{"timeseries": "power-forecast", "points_written": 2},
{"timeseries": "iceloss-forecast", "points_written": 1}
]
}This is exactly the knowledge_time you saw in create a timeseries and query a timeseries — published runs are where it comes from.
List runs
import requests
workflow_id = "0f3c9a55-1e22-4d6b-9aa1-7e8b2c4d5f60"
url = f"https://api.rebaseenergy.dev/platform/v3/workflows/{workflow_id}/runs"
headers = {"Authorization": "Bearer <your_api_key>"}
response = requests.get(url, headers=headers)
print(response.json())Each run carries its status, knowledge_time, and — crucially — the workflow_version it was published under. Runs are immutable: updating the workflow later never changes what a past run points to. If no run arrives within the schedule.expected_cadence, the platform can flag the workflow as late.
Update the workflow (new version)
You deployed new forecast code. Bump the source.ref — any change to the workflow creates a new version:
import requests
workflow_id = "0f3c9a55-1e22-4d6b-9aa1-7e8b2c4d5f60"
url = f"https://api.rebaseenergy.dev/platform/v3/workflows/{workflow_id}"
headers = {"Authorization": "Bearer <your_api_key>"}
body = {
"definition": {
"source": {
"repository": "github.com/rebase/storrun-forecast",
"ref": "4c8d7e1",
},
},
}
response = requests.patch(url, headers=headers, json=body)
print(response.json())The response now has "version_number": 2, "is_current": true. Version 1 still exists at GET /platform/v3/workflows/{workflowId}/versions/1, and every run published under it keeps pointing there. Runs published from now on attribute to version 2.
Trace a forecast back to its code
The full loop: take any forecast slice, resolve its provenance, and inspect the exact definition — including the code ref — that produced it.
import requests
headers = {"Authorization": "Bearer <your_api_key>"}
site_id = "4dbb3433-402f-4276-8a55-7f27753f70dc"
# 1. Which run and version produced the latest forecast?
url = f"https://api.rebaseenergy.dev/platform/v3/objects/sites/{site_id}/timeseries/power-forecast"
params = {"knowledge_time": "latest", "include": "provenance"}
data = requests.get(url, headers=headers, params=params).json()
(knowledge_time, prov), = data["provenance"].items()
# 2. Fetch that exact workflow version.
url = f"https://api.rebaseenergy.dev/platform/v3/workflows/{prov['workflow_id']}/versions/{prov['workflow_version']}"
version = requests.get(url, headers=headers).json()
print(version["definition"]["source"]) # {"repository": "github.com/rebase/storrun-forecast", "ref": "9f31c2a"}No matter how many times the workflow is edited, every knowledge_time in every forecast remains traceable to the run, the frozen definition, and the exact code revision that produced it.
Open questions
Publishing points inline keeps a run atomic, but caps payload size — do we need an open → write → complete lifecycle for long-running or large outputs? Should a publisher be able to pin a version explicitly instead of attributing to whatever is current (a run from old code could arrive after a version bump)? What does a failed run publish look like — no points plus an error, or partial outputs? Named columns flatten a forecast's quantiles cleanly; if a series ever needs a second axis too (say quantile × horizon), do the column names encode it (q50_D1) and stay opaque to the platform, or do we model that differently? And is schedule.external: false a real case (platform-managed execution), or is every workflow external for now?