Relationships
Audience: KRL developers and engine operators
Engine version: 1.6+ (DID-based relationships); legacy ECI path still supported
Ruleset: io.picolabs.subscription (installed on every pico)
A relationship is the user-facing name for a pico-to-pico connection. Prior to engine version 1.6.2, relationships were called subscriptions.
Formation uses wrangler:relationship (legacy: wrangler:subscription). State lives in ruleset io.picolabs.subscription; queries and the module RID keep that name for compatibility.
New rulesets should use
use module io.picolabs.subscription alias relationship
to load the subscriptions module.
Compatibility note:
Prefer (1.6.2+) | Still works |
|---|---|
|
|
|
|
Relationships tab |
|
What is a relationship?
A relationship is a pairwise connection between two picos. These picos may be in the same pico mesh, on different pico meshes, or on different engines. Parent and child picos talk through family channels (parent and child) and do not need a relationship. relationships exist so unrelated picos — your pico and a community pico, two peer meshes, a service and its clients — can exchange events and queries under explicit channel policy.
Each side of a relationship keeps a relationship record: who the remote party is, how to reach them, and which local Rx channel enforces what they are allowed to do. relationships move through outbound pending, inbound pending, and established states until both sides agree.
Identity: three identifiers (1.6)
Since pico-engine 1.6, relationships use two kinds of DIDs in addition to classic ECIs:
Identifier | Role | When you use it |
|---|---|---|
did:webvh | Portable pico identity — who the actor is | Creating a DID-based relationship ( |
did:peer | Private relationship identity — this relationship pair | After intro is approved; ongoing encrypted traffic |
ECI (channel id) | Local policy gate on this engine | Rx channel on each pico; legacy remote Tx ECI |
In general, ECIs are for legacy applications. All new relationships use DIDs.
The did:webvh did is used for introductions. Use did:peer for all other inter-pico communications. Peer DIDs and wellKnown ECIs should not be used when starting a new relationship.
Every pico exposes its did:webvh at:
GET /picos/{picoId}/did.jsonlQuery it in KRL with wrangler:myDid() or copy it from the developer UI relationships → Identity panel. Be sure that “public intro” is enabled on the did:webvh DID to use it across meshes or engines.
relationship lifecycle
Both DID-based and legacy relationships follow the same state pattern:
State | Initiator sees | Recipient sees |
|---|---|---|
Outbound pending | Waiting for peer | — |
Inbound pending | — | Approve or ignore |
Established | Remote party connected | Remote party connected |
DID-based relationships (recommended)
DID-based relationships is the default path for new work. Formation uses a SKY intro handshake. Established relationships store Tx_did and Rx_did instead of a remote Tx ECI.
Formation flow
Initiator — raise wrangler:relationship:
Attribute | Value |
|---|---|
|
|
| Recipient's did:webvh (from |
| Optional human label (shown in Established list) |
| Optional relationship tags |
| Usually |
Recipient — approve with wrangler:pending_relationship_approval (or configure auto-accept).
Do not use wellKnown_Tx on a DID-based create. Do not use a did:peer at create time — the engine mints pairwise DIDs during intro.
What to paste where (same engine)
You have… | DID-based relationship create | Legacy relationship create |
|---|---|---|
Recipient's did:webvh | ✅ | ❌ |
Recipient's wellKnown_Rx ECI | ❌ | ✅ |
Recipient's did:peer | ❌ (not yet) | ❌ |
On the same engine, the runtime resolves did:webvh locally and runs the intro without HTTP when both picos are loaded.
Established relationship (DID-based)
Field | Meaning |
|---|---|
| relationship UUID (stable on both sides) |
| Human label |
|
|
| Remote party's did:peer |
| Local did:peer for this sub |
| Local ECI — policy is enforced here |
| Set when remote pico is on another engine |
Sending queries and events
Query a remote pico by DID or via the relationship ruleset:
// By DID (wrangler routes over established sub)
wrangler:picoQuery(<did_webvh_or_peer>, "io.picolabs.wrangler", "id", {})
// By relationship Id
relationship:queryOnSub(<subId>, "io.picolabs.wrangler", "id", {})Send an event:
event:send({
"did": "<peer_did or webvh>",
"domain": "wrangler",
"type": "ping",
"attrs": {}
})Or with the relationship record:
event:send({
"sub": relationship,
"domain": "wrangler",
"type": "ping",
"attrs": {}
})When bus.layer2 is true, the engine uses verified local dispatch if both picos share a mesh on this engine; otherwise it uses sky over DIDComm to the peer's ingress URL.
How traffic is routed
Once a relationship is established, the engine has to decide how to deliver a query or event to the other pico. That depends on three things: whether the relationship is DID-based or classic (ECI-based), whether caller and recipient belong to the same mesh (same passkey root on one engine), and whether the recipient is local or on another engine. DID-based subs on the same mesh usually stay on local dispatch—no HTTP, no DIDComm on the wire. Cross-mesh or cross-engine DID-based traffic uses SKY over DIDComm. Classic subs still send to the remote Tx ECI, with an optional Tx_host when the peer is on another machine. In every case, authorization is enforced on the recipient’s relationship Rx channel (classic subs may also involve the remote Tx channel).
The diagram above is the decision path for a single query or event. The table below summarizes the same idea by relationship type: parent and child picos never use relationships; established relationships choose local vs DIDComm vs classic ECI routing as above; and OAuth integrators are a separate path entirely (HTTP /sky/* with a Bearer token, not a relationship). Use the table when you’re asking “how do these two picos talk?” rather than stepping through the flowchart node by node.
Relationship | relationship | Transport | Policy |
|---|---|---|---|
Parent → child | None (family) | Local only | Family channel |
Same mesh, same engine | DID-based or legacy | Local when intra-mesh | Sub Rx channel |
Cross-mesh, same engine | DID-based | DIDComm → ingress | Sub Rx channel |
Cross-engine | DID-based | DIDComm → peer URL | Sub Rx channel |
Legacy ECI sub | Legacy | HTTP / event:send to Tx | Tx and Rx policies |
Edit policy: Developer UI → Channels tab, or relationships → expand sub → edit Rx policy (links to ?focus=<rxEci>).
Cross-engine relationships
Requirements:
Each engine has a stable, peer-reachable
PICO_ENGINE_BASE_URL(embedded in did:webvh at pico creation).Recipient (usually the root pico) has
publicIntro: true(default for roots; child picos defaultfalse).Initiator uses
target_did= recipient's did:webvh.
The formation sequence matches same-engine DID-based; intro, response, and traffic use encrypted DIDComm to:
POST {peerBase}/sky/event/{ingressEci}/none/dido/didcomm_messageTwo-engine demo (one machine)
# Terminal 1
PICO_ENGINE_HOME=/tmp/pico-engine-a PORT=3001 npm start
# Terminal 2
PICO_ENGINE_HOME=/tmp/pico-engine-b PORT=3002 npm startRegister a passkey on both UIs.
On engine B: relationships → Identity → copy did:webvh.
On engine A: relationships → DID-based create → paste B's DID → submit.
On B: Inbound pending → Approve.
On A: confirm Established shows
Tx_did,Rx_did,Tx_host.
Debugging
The following table can help when debugging failed relationships.
Symptom | Check |
|---|---|
Intro never arrives | Recipient |
DID resolution failed | Fetch |
401 on DIDComm | Re-approve sub; ingress ECI provisioned |
Legacy ECI relationships
The original relationship model (pre–DID-based) remains fully supported for existing rulesets and tutorials.
Recipient exposes wellKnown_Rx (created at engine start; visible in UI Identity panel).
Initiator raises
wrangler:relationshipwithwellKnown_Tx= recipient's wellKnown_Rx ECI (nolayer2).Classic
new_subscription_request/ approval flow.Established relationship has Tx and Rx ECIs;
layer2is false.
You can find the old documentation for pre-1.6 relationships here.
Developer UI (relationships tab)
The Relationships tab in the developer UI allows you to manage relationships for a pico: its portable DID, whether it accepts unsolicited intros, relationships in each lifecycle state, and the form to request a new one. The screenshot below is from a child pico with Public intro enabled and one relationship in each state (established, inbound pending, outbound pending).
Pico chrome
Pico name header (
child_of_mine) and tabs: About, Rulesets, Channels, Logging, Testing, relationships (active)
Identity
did:webvh (myDid) — full DID with copy button
Legacy wellKnown_Rx ECI — for classic (ECI-based) relationships
Public intro — checkbox to accept unsolicited SKY intro on your did:webvh (checked here)
Established
One row: name or Id (
cmsowgyig0001uqdt5xsndvw5) +didbadge
(Checkbox expands detail: Tx_did, Rx_did, Rx policy, test send, delete.)
Inbound pending
One row: test_inbound (name) + Id +
didbadge
(Expand to review; Accept approves the relationship.)
Outbound pending
One row: test_outbound (name) + Id +
didbadge
(Expand to review; cancel from detail if needed.)
New relationship
Toggle: DID-based | Legacy (ECI)
target_did, name, Rx_role, Tx_role, channel_type (default
Tx_Rx), password (optional)Request relationship button
Use name fields so established and pending lists stay readable; the relationship Id is still shown (muted) for debugging. Switch to Legacy (ECI) to create with wellKnown_Tx instead of target_did. Rows are collapsed in the screenshot — expand any relationship to see DIDs, edit Rx policy (link to Channels), or send a test event on established DID-based subs.
KRL and wrangler API
relationship logic lives in two rulesets on every pico: io.picolabs.subscription owns the relationship records (pending, established, wellKnown_Rx) and most lifecycle rules; io.picolabs.wrangler exposes identity helpers and picoQuery, the usual way rulesets call remote picos. You raise wrangler events to create, approve, or cancel relationships; you query relationship and wrangler functions to read state; once a sub is established, you query or send events to the remote pico through the relationship (by DID, by relationship map, or via send_event_on_subs with subID / roles.).
Identity (wrangler queries and events)
Every pico has a portable did:webvh. Use these before or alongside relationship work:
Query / action | Purpose |
|---|---|
| This pico's did:webvh — give this to partners as their |
| Whether unsolicited SKY intro is accepted on that DID |
| Toggle public intro (UI uses event |
| On an inbound cross-pico event, the sender's did:webvh (when provided by the engine) |
Root picos default to public intro on; child picos default off.
Reading relationship state (io.picolabs.subscription)
Query | Returns |
|---|---|
| List of established relationship maps |
| Filter by field (same for any relationship key) |
| Inbound pending (awaiting your approval) |
| Outbound pending (awaiting peer) |
| Legacy intro channel map (for ECI-based subs) |
| Auto-accept rules (legacy password/role matching) |
Each relationship is a map: Id, name, Tx_role, Rx_role, local Rx (policy gate), and either Tx_did / Rx_did (DID-based) or Tx / Tx_host (classic).