Skip to main content
Run your own R code inside Aire Labs models. A container function is a Docker image that reads inputs from the model, runs your computation, and writes results back. You bring the R code; the platform handles the rest. No Docker experience is required. Start by running a working example that computes Levelized Cost of Energy (LCOE) from bundled solar and wind cost data, then adapt it for your own computation. Every step has a checkpoint so you can confirm it worked. Terminal commands assume macOS or Linux.

1. Prerequisites

Docker runs your R code in an isolated container so it behaves the same on your laptop and on the Aire Labs platform. Install one of these:
Before continuing, make sure OrbStack or Docker Desktop is running — look for its icon in your menu bar. If it’s not open, launch it now and wait for it to finish starting.
Open a terminal and run:
docker run --rm hello-world
You should see Hello from Docker! in the output. If you get Cannot connect to the Docker daemon, open OrbStack or Docker Desktop and try again.

2. Quick start

Clone the example and build the Docker image. This downloads R and installs the jsonlite package inside the container. It takes about 2 minutes the first time; subsequent builds are fast.
git clone https://github.com/airelabsresearch/integration-language-r.git
cd integration-language-r

docker build -t lcoe-r .
The last line of output should say something like naming to docker.io/library/lcoe-r done.
Apple Silicon (M1/M2/M3)If you see Base image was pulled with platform "linux/amd64", expected "linux/arm64", your local Docker cache has the wrong architecture for the base image. Pull the correct variant and rebuild:
docker pull --platform linux/arm64 rocker/r-base:latest
docker build -t lcoe-r .
This is not a blocking error on all setups — Docker can sometimes emulate amd64 on arm64 — but pulling the native image avoids the warning and runs faster.
Run the container with a sample input file. This simulates what the Aire Labs platform does — it passes input values as JSON, runs your R code, and reads the results back.
mkdir -p /tmp/airelabs
cp fixtures/hook-input.json /tmp/airelabs/hook-input.json

docker run --rm \
  -v /tmp/airelabs:/airelabs \
  -e AIRELABS_HOOK_INPUT_PATH=/airelabs/hook-input.json \
  -e AIRELABS_HOOK_OUTPUT_PATH=/airelabs/hook-output.json \
  lcoe-r
You should see: OK — dataset=solar, year=2027, lcoe=43.39 USD/MWh
Inspect the output file your R code produced:
cat /tmp/airelabs/hook-output.json
Expected output:
{"results":[{"name":"dataset","string":"solar"},{"name":"capex","number":{"value":"960.00","unit":"USD/kW"}},{"name":"opex","number":{"value":"16.50","unit":"USD/kW/yr"}},{"name":"capacity_factor","number":{"value":"0.28"}},{"name":"lcoe","number":{"value":"43.39","unit":"USD/MWh"}}]}
That’s it. The platform reads this JSON after your container exits and writes each result back into the model.

3. How it works

When a model contains a container function block, the Aire Labs compute engine resolves the input values, writes them to a JSON file, runs your container, reads the output JSON, and writes the results back into the model. The docker run command you just ran simulates that same flow locally. See the Docker getting started guide for more on volumes (-v) and environment variables (-e).

The input file

Open fixtures/hook-input.json. This is the format the platform uses to pass values from your model into your R code:
fixtures/hook-input.json
{
  "parametersV1": [
    { "name": "dataset",        "string": "solar" },
    { "name": "target_year",    "number": { "value": "2027" } },
    { "name": "discount_rate",  "number": { "value": "0.08" } },
    { "name": "lifetime_years", "number": { "value": "25", "unit": "years" } }
  ]
}
Each parameter has a name and one typed value field. Numbers are always strings ("0.08" not 0.08) to preserve decimal precision. The optional unit field carries the unit of measure.
Each parameter name corresponds to an input node in your Aire Labs model. When you wire a container function block into a model, you map model terms to these parameter names. The platform reads the current value of each mapped term and passes it to your code via this JSON file.

The output file

Your R code writes a JSON file with the same structure, using a results array instead of parametersV1:
/tmp/airelabs/hook-output.json
{
  "results": [
    { "name": "dataset",         "string": "solar" },
    { "name": "capex",           "number": { "value": "960.00",  "unit": "USD/kW" } },
    { "name": "opex",            "number": { "value": "16.50",   "unit": "USD/kW/yr" } },
    { "name": "capacity_factor", "number": { "value": "0.28" } },
    { "name": "lcoe",            "number": { "value": "43.39",   "unit": "USD/MWh" } }
  ]
}
Your code defines the output names. Each name in the results array becomes an output on the container function block in your Aire Labs model. The platform reads this file after your container exits and writes each value back into the model, making them available to downstream formulas.
Your container runs in a secure sandbox on the Aire Labs platform: read-only filesystem (except /tmp), no network access, and configurable memory/CPU limits. The only way to communicate with the platform is through these JSON files.

4. The R code

The example has three R files. Open each one as you read through this section.

main.R — the entry point

Reads input parameters, loads cost assumptions from a bundled CSV, computes LCOE, and writes the results. The JSON plumbing is handled by helpers in R/airelabs.R:
main.R
params <- read_hook_input()

dataset        <- require_string(params, "dataset")
target_year    <- as.integer(require_number(params, "target_year"))
discount_rate  <- require_number(params, "discount_rate")
lifetime_years <- as.integer(require_number(params, "lifetime_years", expected_unit = "years"))

costs <- load_cost_assumptions(dataset, target_year)
lcoe  <- compute_lcoe(costs$capex, costs$opex, costs$capacity_factor,
                       discount_rate, lifetime_years)

write_hook_output(list(
  string_result("dataset",          dataset),
  number_result("capex",            costs$capex,           "USD/kW"),
  number_result("opex",             costs$opex,            "USD/kW/yr"),
  number_result("capacity_factor",  costs$capacity_factor),
  number_result("lcoe",             lcoe,                  "USD/MWh")
))
The full main.R wraps this in a tryCatch so the container exits with code 1 on any error, which tells the platform the invocation failed.

R/model.R — the LCOE computation

Pure R. No knowledge of JSON, Docker, or the Aire Labs platform. You can source() it in RStudio and call compute_lcoe() directly.
R/model.R
HOURS_PER_YEAR <- 8760

capital_recovery_factor <- function(discount_rate, lifetime_years) {
  r <- discount_rate
  n <- lifetime_years
  r * (1 + r)^n / ((1 + r)^n - 1)
}

compute_lcoe <- function(capex, opex, capacity_factor, discount_rate, lifetime_years) {
  crf <- capital_recovery_factor(discount_rate, lifetime_years)
  lcoe_per_kwh <- (capex * crf + opex) / (capacity_factor * HOURS_PER_YEAR)
  lcoe_per_kwh * 1000
}
Keep your business logic in model.R with no platform dependencies, and keep the JSON plumbing in airelabs.R. This means you can develop and test your model in RStudio exactly the way you normally would, then wrap it for Aire Labs without changing it.

R/data_lookup.R — loading bundled data

Containers have no network access, so reference data must be built into the Docker image. The data/ directory contains CSV files with year-by-year cost assumptions for solar and wind. The load_cost_assumptions() function looks up a row by dataset name and year, and stops with a clear error if there’s no match.

R/airelabs.R — JSON helpers

Handles reading the input JSON and writing the output JSON. Copy it as-is into any container function project. The key functions:
  • read_hook_input() — reads input JSON, returns a data frame
  • require_number(), require_string() — extract a parameter by name (stops if missing)
  • number_result(), string_result(), error_result() — build result entries
  • write_hook_output() — writes results as JSON

5. Different inputs

Copy a different fixture into /tmp/airelabs/hook-input.json and re-run the same docker run command from the quick start.

Wind instead of solar

cp fixtures/hook-input-wind.json /tmp/airelabs/hook-input.json

docker run --rm \
  -v /tmp/airelabs:/airelabs \
  -e AIRELABS_HOOK_INPUT_PATH=/airelabs/hook-input.json \
  -e AIRELABS_HOOK_OUTPUT_PATH=/airelabs/hook-output.json \
  lcoe-r
This should show lcoe=44.81 USD/MWh for wind 2030.

Invalid discount rate (error result)

This fixture passes a negative discount rate. Instead of crashing, the container returns an error result on the LCOE output — the platform marks that cell as an error while the other outputs still get values.
cp fixtures/hook-input-bad-rate.json /tmp/airelabs/hook-input.json

docker run --rm \
  -v /tmp/airelabs:/airelabs \
  -e AIRELABS_HOOK_INPUT_PATH=/airelabs/hook-input.json \
  -e AIRELABS_HOOK_OUTPUT_PATH=/airelabs/hook-output.json \
  lcoe-r
The output JSON will have "lcoe": { "error": "INVALID_DISCOUNT_RATE" } — not a number.

Unknown dataset (hard error)

cp fixtures/hook-input-unknown-dataset.json /tmp/airelabs/hook-input.json

docker run --rm \
  -v /tmp/airelabs:/airelabs \
  -e AIRELABS_HOOK_INPUT_PATH=/airelabs/hook-input.json \
  -e AIRELABS_HOOK_OUTPUT_PATH=/airelabs/hook-output.json \
  lcoe-r
This should fail with: Error: unknown dataset 'geothermal' — available datasets: solar, wind. Unlike the invalid discount rate (which returned partial results), this is a hard error — exit code 1, no output file written.

Run the tests

docker run --rm lcoe-r Rscript tests/test_model.R
docker run --rm lcoe-r Rscript tests/test_main.R
Both should end with All model tests passed. or All integration tests passed.

6. Writing your own function

Replace the example with your own computation:
  1. Write your model in R/model.R. Keep it pure R — no JSON, no Docker, no platform code. It should take arguments and return a list. Develop and test it in RStudio the way you normally would.
  2. Edit main.R to read the parameters your model needs (using require_number, require_string, etc.) and write the results it produces (using number_result, string_result, etc.).
  3. Create a fixture file in fixtures/ with the parameters your model expects. Use an existing fixture as a template.
  4. If you need more R packages, add them to the RUN Rscript -e line in the Dockerfile:
Dockerfile
RUN Rscript -e "install.packages(c('jsonlite', 'dplyr'), repos='https://cloud.r-project.org')"
  1. Rebuild and test:
docker build -t my-function .
cp fixtures/my-input.json /tmp/airelabs/hook-input.json

docker run --rm \
  -v /tmp/airelabs:/airelabs \
  -e AIRELABS_HOOK_INPUT_PATH=/airelabs/hook-input.json \
  -e AIRELABS_HOOK_OUTPUT_PATH=/airelabs/hook-output.json \
  my-function

cat /tmp/airelabs/hook-output.json
The R/airelabs.R helper module works for any container function — copy it as-is into new projects.

7. The Dockerfile

Here is what the Dockerfile does, line by line:
Dockerfile
FROM rocker/r-base:latest

RUN Rscript -e "install.packages('jsonlite', repos='https://cloud.r-project.org')"

WORKDIR /app
COPY . .

CMD ["Rscript", "main.R"]
  • FROM rocker/r-base:latest — starts from the Rocker base image, which includes R and common system libraries.
  • RUN Rscript -e "install.packages(...)" — installs R packages during the build, so they’re available at runtime without network access.
  • COPY . . — copies your project (R files, CSV data, fixtures) into the image.
  • CMD ["Rscript", "main.R"] — the default command when the container starts.

Choosing a base image

The Rocker project provides several image variants:
ImageWhat’s includedUse when
rocker/r-baseMinimal RSimple scripts with few packages
rocker/r-ver:4.4.0Pinned R versionYou need reproducible builds
rocker/tidyverseR + dplyr, ggplot2, tidyr, etc.Using tidyverse packages

Adding packages

To add R packages, extend the RUN Rscript -e line. Some packages (like httr or xml2) also need system C libraries — see the Rocker extending images guide. For reproducible builds, pin your R version with rocker/r-ver:4.4.0 instead of :latest.

8. Common questions

Can I use multiple R files? Yes. There is no limit to the number of R files in your container. The example uses three files (main.R, R/model.R, R/airelabs.R) and you can add as many as you need. Use source() in main.R to load them. Everything inside your project directory gets copied into the Docker image by COPY . . in the Dockerfile, so any file structure that works locally will work in the container. How do output names work? Output names are entirely up to you. The name field in each result entry (e.g. "lcoe") is defined by your R code and determines how the output appears in Aire Labs under the block that represents your container function. You do not need to pre-declare them anywhere — the platform reads the output JSON your container produces and creates the corresponding outputs on the block automatically. Can inputs come from the Aire Labs model? Yes — that is exactly how inputs work. When you configure a container function block in the model, you map model terms to input parameter names. The platform reads the current value of each mapped term and passes it to your code via the input JSON file. Inputs can be any term in the model: manual inputs, formula results, or outputs from other container functions.

9. What’s next

Once your container function works locally — it reads fixture files, produces the correct output, and passes your tests — the next step is publishing it to the Aire Labs platform so it can run inside your models.
Coming soonA deployment guide covering how to connect your GitHub repository, configure the build pipeline, and wire your container function into an Aire Labs model is in progress. In the meantime, reach out to support@airelabs.com and we’ll help you get set up.

10. Reference

Supported value types

Both input parameters and output results support these types. Set exactly one value field per entry.
FieldJSON formatExample
number{ value, unit? }{ "value": "12500000", "unit": "USD" }
numberArray{ values, unit? }{ "values": ["100", "200"], "unit": "USD/MWh" }
stringstring"Solar Farm Alpha"
stringArray{ values }{ "values": ["wind", "solar"] }
booleanstring"true"
booleanArray{ values }{ "values": ["true", "false"] }
datestring (YYYY-MM-DD)"2025-03-12"
dateArray{ values }{ "values": ["2025-03-12", "2025-09-01"] }
timestampstring (ISO 8601)"2025-03-12T14:30:00Z"
timestampArray{ values }{ "values": ["2025-03-12T14:30:00Z"] }
errorstring"INVALID_DISCOUNT_RATE"
Numbers are always decimal strings — no scientific notation, no thousands separators. Booleans are the literal strings "true" or "false", not JSON booleans. The error type signals that a specific output could not be computed.

Environment variables

The platform sets these before your container starts:
VariableDescription
AIRELABS_HOOK_INPUT_PATHPath to the input JSON file
AIRELABS_HOOK_OUTPUT_PATHWhere to write the output JSON file
AIRELABS_ORGANIZATION_IDOrganization that owns the project
AIRELABS_PROJECT_IDProject containing the model
AIRELABS_SCENARIO_IDScenario being computed
AIRELABS_BLOCK_IDContainer function block node ID
AIRELABS_INVOCATION_IDUnique ID for this invocation

R + JSON gotchas

The helpers in airelabs.R handle these, but if you’re writing your own JSON serialization:
  • Always pass auto_unbox = TRUE to write_json() — otherwise R wraps single values in arrays. See the jsonlite quickstart.
  • Build results with list(), not data.frame().
  • Use format(..., scientific = FALSE) for numbers — the platform rejects scientific notation.
  • Booleans are the strings "true" / "false", not R’s TRUE / FALSE.

Reproducible R environments

For projects with many dependencies, use renv to lock package versions. See the renv Docker vignette for a complete example.

JSON schemas

Machine-readable schemas for editor autocompletion and validation:
  • Input: https://airelabs.studio/schemas/v1/airelabs.v1.HookInput.jsonschema.strict.bundle.json
  • Output: https://airelabs.studio/schemas/v1/airelabs.v1.HookOutput.jsonschema.strict.bundle.json

Further reading