Relationships

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

Prefer (1.6.2+)

Still works

wrangler:relationship

wrangler:subscription

relationship:established() (with module alias)

subscription:established()

Relationships tab

/subscriptions URL

What is a relationship?

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 pendinginbound 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

Identifier

Role

When you use it

did:webvh

Portable pico identity — who the actor is

Creating a DID-based relationship (target_did); public intro; VC subject

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.jsonl

Query 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:

Outbound and Inbound-2026-08-13-180404.png

 

State

Initiator sees

Recipient sees

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

Subscription Approval Flow-2026-08-13-180634.png

 

 

Initiator — raise wrangler:relationship:

Attribute

Value

Attribute

Value

layer2

true

target_did

Recipient's did:webvh (from wrangler:myDid or UI)

name

Optional human label (shown in Established list)

Tx_roleRx_role

Optional relationship tags

channel_type

Usually Tx_Rx

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

You have…

DID-based relationship create

Legacy relationship create

Recipient's did:webvh

✅ target_did

Recipient's wellKnown_Rx ECI

✅ wellKnown_Tx

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

Field

Meaning

Id

relationship UUID (stable on both sides)

name

Human label

layer2

true

Tx_did

Remote party's did:peer

Rx_did

Local did:peer for this sub

Rx

Local ECI — policy is enforced here

Tx_host

Set when remote pico is on another engine

subscription-bus.png

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).

Untitled diagram-2026-08-11-222221.png

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

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:

  1. Each engine has a stable, peer-reachable PICO_ENGINE_BASE_URL (embedded in did:webvh at pico creation).

  2. Recipient (usually the root pico) has publicIntro: true (default for roots; child picos default false).

  3. 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_message

Two-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 start
  1. Register a passkey on both UIs.

  2. On engine B: relationships → Identity → copy did:webvh.

  3. On engine A: relationships → DID-based create → paste B's DID → submit.

  4. On B: Inbound pending → Approve.

  5. On A: confirm Established shows Tx_didRx_didTx_host.

Debugging

The following table can help when debugging failed relationships.

Symptom

Check

Symptom

Check

Intro never arrives

Recipient publicIntro; port/firewall

DID resolution failed

Fetch {base}/picos/{id}/did.jsonl

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.

  1. Recipient exposes wellKnown_Rx (created at engine start; visible in UI Identity panel).

  2. Initiator raises wrangler:relationship with wellKnown_Tx = recipient's wellKnown_Rx ECI (no layer2).

  3. Classic new_subscription_request / approval flow.

  4. Established relationship has Tx and Rx ECIs; layer2 is 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) + did badge
    (Checkbox expands detail: Tx_did, Rx_did, Rx policy, test send, delete.)

Inbound pending

  • One row: test_inbound (name) + Id + did badge
    (Expand to review; Accept approves the relationship.)

Outbound pending

  • One row: test_outbound (name) + Id + did badge
    (Expand to review; cancel from detail if needed.)

New relationship

  • Toggle: DID-based | Legacy (ECI)

  • target_didnameRx_roleTx_rolechannel_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

Query / action

Purpose

wrangler:myDid()

This pico's did:webvh — give this to partners as their target_did

wrangler:publicIntro()

Whether unsolicited SKY intro is accepted on that DID

wrangler:set_publicIntro(true | false)

Toggle public intro (UI uses event wrangler:set_public_intro)

wrangler:callerDid()

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

Query

Returns

relationship:established()

List of established relationship maps

relationship:established("Tx_role", "owner")

Filter by field (same for any relationship key)

relationship:inbound()

Inbound pending (awaiting your approval)

relationship:outbound()

Outbound pending (awaiting peer)

relationship:wellKnown_Rx()

Legacy intro channel map (for ECI-based subs)

relationship:autoAcceptConfig()

Auto-accept rules (legacy password/role matching)

Each relationship is a map: IdnameTx_roleRx_role, local Rx (policy gate), and either Tx_did / Rx_did (DID-based) or Tx / Tx_host (classic).