How to add a custom relation to a 12-factor charm

The 12-factor tooling ships with a set of built-in Juju relations (see Observability and relations). When you need a relation that is not built in, use the public paas_charm.relations.CustomRelation extension API to add one without importing or subclassing paas-charm internals.

A custom relation can:

  • Contribute environment variables to the workload.

  • Declare the workload not-ready (a missing required relation, or a related relation with missing or invalid data, blocks the workload with a BlockedStatus).

  • Trigger the reconcile/restart path on relation events, optionally re-running database migrations.

Use a custom relation when you need integration data that does not belong in the framework, or when you want to drive a relation that the framework does not support yet. The most relevant relations will be added to the framework, while others never will — this extension point gives you a stable, supported path for the latter.

Prerequisites

  • A 12-factor charm generated by a Charmcraft framework extension.

  • The endpoint you want to integrate with declared in charmcraft.yaml under requires.

  • Any charm library the relation needs fetched into the charm. The framework does not guard missing author libraries; a missing library surfaces as an ordinary charm error.

Register a custom relation

A custom relation is a subclass of CustomRelation that you register on the charm via the custom_relations class attribute:

src/charm.py
import ops
from charms.temporal_k8s.v0.temporal_host_info import TemporalHostInfoRequirer

import paas_charm.flask
from paas_charm.relations import (
    CustomRelation,
    InvalidRelationDataError,
    OnChange,
    RelationDataError,
)


class TemporalRelation(CustomRelation):
    relation_name = "temporal-host-info"

    def setup(self, on_change: OnChange) -> None:
        self._requirer = TemporalHostInfoRequirer(self.charm)
        self._framework_observe(
            self._requirer.on.temporal_host_info_changed,
            on_change,
        )
        self._framework_observe(
            self._requirer.on.temporal_host_info_unavailable,
            on_change,
        )

    def ensure_ready(self) -> None:
        if self._requirer is None or self._requirer.relation is None:
            raise RelationDataError(
                "Temporal relation not ready", relation=self.relation_name
            )
        self._connection_info()

    def gen_environment(self) -> dict[str, str]:
        connection_info = self._connection_info()
        if connection_info is None:
            return {}
        host, port = connection_info
        return {"TEMPORAL_HOST": host, "TEMPORAL_PORT": str(port)}

    def _connection_info(self) -> tuple[str, int] | None:
        if self._requirer is None or self._requirer.relation is None:
            return None
        try:
            host = self._requirer.host
            port = self._requirer.port
        except (TypeError, ValueError) as exc:
            raise InvalidRelationDataError(
                "invalid Temporal port", relation=self.relation_name
            ) from exc
        if not host or port is None:
            raise InvalidRelationDataError(
                "missing Temporal host or port", relation=self.relation_name
            )
        if not 1 <= port <= 65535:
            raise InvalidRelationDataError(
                "Temporal port must be between 1 and 65535",
                relation=self.relation_name,
            )
        return host, port


class FlaskCharm(paas_charm.flask.Charm):
    custom_relations = [TemporalRelation]


if __name__ == "__main__":
    ops.main.main(FlaskCharm)

Declare the endpoint in charmcraft.yaml. The optional flag is the single source of truth for whether the relation is required:

charmcraft.yaml
requires:
  temporal-host-info:
    interface: temporal-host-info
    optional: true
    limit: 1

Workload integration behavior:

Relation Status

Workload Impact

No relation and optional: true

the workload runs without Temporal configuration

A related Temporal server provides valid connection data

TEMPORAL_HOST and TEMPORAL_PORT appear in the workload environment and the service is (re)started

A related app provides incomplete or invalid connection data

BlockedStatus("RelationDataError: <exception message>"). This applies once the relation is established, whether it is optional or required.

Relation removed

removes environment variables and restarts the workload.

Add the Temporal charm library to the charm’s charm-libs before building:

charmcraft.yaml
charm-libs:
  - lib: temporal-k8s.temporal_host_info
    version: "0"

Set optional: false instead when the application cannot operate without Temporal. A missing relation will then produce BlockedStatus("missing integrations: temporal-host-info").

Implement custom relation API

For lightweight implementations, custom relation logic may be placed directly in src/charm.py.

Method

Env-var relation

Side-effect relation

Comments

setup(on_change)

Wire requirer events → on_change().

Wire relation events; instantiate a requirer or call a require_* helper.

ensure_ready()

Override → raise RelationDataError (or InvalidRelationDataError for malformed data) when not ready.

Leave at default (no-op) — never blocks the workload.

gen_environment()

Override → read the databag, return an env-var mapping.

Leave at default (returns {}).

Ensure it never raises an exception intentionally

setup(on_change) is the only required method. The framework injects an on_change callback (typed by the OnChange protocol). Use the helper method self._framework_observe(bound_event, on_change, rerun_migrations=...) to observe relation events: pass rerun_migrations=True when a relation event should also re-run database migrations.

Read more

See Custom relations API to learn more about Custom Relations API, and Observability and relations for the information about built-in relations.

A complete, runnable Flask charm that wires a custom temporal-host-info relation ships under examples/flask/ (declared with optional: true in charmcraft.yaml).