Linky logoLinky
Docs navigation

Launch bundles — personalize

Personalize a Linky

One Linky, different tabs per viewer. Attach a resolutionPolicyand Linky evaluates it against the viewer's identity on every click to /l/[slug].

Policy shape

type ResolutionPolicy = {
  version: 1;
  rules: Rule[];
};

type Rule = {
  id: string;                  // minted for you if you omit it
  name?: string;               // your private label; viewer sees it only if showBadge
  when: Condition;             // what has to be true for this rule to match
  tabs: { url: string; note?: string }[];
  stopOnMatch: boolean;        // default: true — first match wins and evaluation stops
  showBadge: boolean;          // default: false — rule name stays private
};

Omit id and Linky mints one for you. Omit stopOnMatch and the first match wins; omit showBadge and the rule name stays private to you.

Viewer fields

Single-value fields: email, emailDomain, userId, githubLogin, googleEmail. Each holds one value per viewer.

Plural fields (hold multiple values per viewer): orgIds, orgSlugs. These are the viewer's full organization membership list — not just whichever workspace they have active.

in is the only operator you can use against a plural field. equals, endsWith, and exists against orgIds / orgSlugs are rejected with a 400 — use in with a single-element value array instead.

What each field means and where its value comes from is documented on the Identity page.

Operators

OpExampleNotes
always{ "op": "always" }Matches every viewer. Useful as a catch-all final rule.
anonymous{ "op": "anonymous" }Matches viewers who aren't signed into Linky.
signedIn{ "op": "signedIn" }Matches any signed-in viewer, regardless of identity.
equals{ "op": "equals", "field": "email", "value": "alice@acme.com" }Exact-match on a single-value field.
in{ "op": "in", "field": "orgSlugs", "value": ["acme", "acme-staging"] }Matches if the viewer's org membership list overlaps the value list, or — on a single-value field — if the viewer's value is in the list.
endsWith{ "op": "endsWith", "field": "emailDomain", "value": "acme.com" }Suffix match on a single-value field.
exists{ "op": "exists", "field": "githubLogin" }Matches when the viewer has any value for that field.
and{ "op": "and", "of": [ { "op": "signedIn" }, { "op": "endsWith", "field": "emailDomain", "value": "acme.com" } ] }All branches must match.
or{ "op": "or", "of": [ { "op": "equals", "field": "email", "value": "alice@acme.com" }, { "op": "equals", "field": "email", "value": "bob@acme.com" } ] }Any branch matches.
not{ "op": "not", "of": [ { "op": "anonymous" } ] }Invert a single nested condition.

How Linky evaluates your rules

  • Rules are checked top-to-bottom. With stopOnMatch: true(the default) the first matching rule fires and the viewer gets that rule's tabs. With stopOnMatch: falsethe rule's tabs are appended and evaluation continues down the list.
  • A rule that references a field the viewer doesn't have just fails to match — it never errors.
  • An empty or missing policy skips evaluation entirely; every viewer gets the public tab set.
  • Rule names are private by default. The viewer only sees the matched rule's name when you set showBadge: true on that rule.
  • When nothing matches, the viewer always gets the public tab set you supplied as urls.

Limits

  • Up to 50 rules per policy.
  • Up to 20 tabs per rule.
  • Condition nesting depth ≤ 4.
  • String values are capped at 512 characters.

The Limits page has the full list alongside plan and rate-limit defaults.

How to author

Dashboard — the Personalize panel at /dashboard/links/[slug] has two modes:

  • Structured — canned operator presets (equals email, endsWith emailDomain, in orgSlugs, anonymous, signedIn) plus a Preview as control that runs your rules against a sample viewer exactly the way /l/[slug] would.
  • Advanced (JSON) — raw policy with validation on Apply. Use this for compound and / or / not.

API / CLI / SDK — attach at create time (see Create) or edit later via PATCH /api/links/:slug (see API). The CLI takes a JSON file with --policy (see CLI), and the SDK takes an object under resolutionPolicy (see SDK).

Worked example

Two rules: signed-in Acme engineers get a personal Linear + GitHub queue with a visible badge; members of partner-co get a separate partner dashboard. Everyone else falls through to the public tab set.

{
  "version": 1,
  "rules": [
    {
      "name": "Engineering team",
      "showBadge": true,
      "when": {
        "op": "and",
        "of": [
          { "op": "signedIn" },
          { "op": "endsWith", "field": "emailDomain", "value": "acme.com" }
        ]
      },
      "tabs": [
        { "url": "https://linear.app/acme/my-issues", "note": "Your queue" },
        { "url": "https://github.com/acme/app/pulls?q=author:@me" }
      ]
    },
    {
      "name": "Partner access",
      "when": {
        "op": "in",
        "field": "orgSlugs",
        "value": ["partner-co"]
      },
      "tabs": [
        { "url": "https://partners.acme.com/dashboard" }
      ]
    }
  ]
}