ShipOS docs
Targeting Top-to-bottom

Targeting Rules

Targeting decides who gets what based on the evaluation context, plan, region, version, anything you pass. Rules run before the base rollout, so you can serve specific cohorts precisely and let everyone else fall through.

The shape

A flag's rules is an ordered array (max 64). Each rule has up to 32 conditions and serves either "on" or "off", optionally gated by its own rolloutPct.

typescript
type Rule = {
  id: string;
  description?: string;
  conditions: Condition[]; // max 32, all must match (AND)
  serve: "on" | "off";
  rolloutPct?: number;     // 0–100, gates this rule's served value
};

type Condition = {
  attribute: string;       // e.g. "plan", "region", "version"
  op: Operator;
  value: unknown;
};

Operators

OperatorMeaning
eq / neqEquals / not equals
in / not_inValue is (not) in a list
containsString contains substring
gt / gte / lt / lteNumeric comparisons
semver_eq / semver_gt / semver_ltSemantic-version comparisons

Evaluation order

Rules are evaluated top to bottom. The first rule whose conditions all match decides the served value, optionally gated by that rule's rolloutPct. If no rule matches, the flag's base enabled + rolloutPct applies. Order your rules most-specific first.

A worked example

Serve the new checkout ON to all internal users, then to Pro users on version 4.2.0 or newer, but only 50% of them, and let everyone else fall through to the base config.

rules.jsonjson
{
  "rules": [
    {
      "id": "rule_internal",
      "description": "Always on for staff",
      "conditions": [
        { "attribute": "email", "op": "contains", "value": "@shipos.app" }
      ],
      "serve": "on"
    },
    {
      "id": "rule_pro_v42",
      "description": "50% of Pro on v4.2+",
      "conditions": [
        { "attribute": "plan", "op": "eq", "value": "pro" },
        { "attribute": "version", "op": "semver_gte", "value": "4.2.0" }
      ],
      "serve": "on",
      "rolloutPct": 50
    }
  ]
}

Passing attributes

Attributes come from the evaluation context you pass to the SDK, the userId and an attributes object. Rules match against these keys.

checkout.tstypescript
const useV2 = await shipos.flag("checkout-v2", {
  userId: "u_42",
  attributes: {
    plan: "pro",
    region: "eu",
    version: "4.3.1",
    email: "dana@shipos.app",
  },
  default: false,
});

Setting rules

Set targeting via the MCP tool or the config API.

text
Claude → set_targeting {
  key: "checkout-v2",
  env: "prod",
  rules: [ { id: "rule_pro_v42", conditions: [...], serve: "on", rolloutPct: 50 } ]
}
  ✅ checkout-v2 @ prod → 1 rule
http
PUT https://app.shipos.app/api/v1/flags/{id}/environments/prod/config
Authorization: Bearer sos_agt_...
Content-Type: application/json

{
  "expectedVersion": 7,
  "rules": [
    { "id": "rule_pro_v42", "conditions": [ ... ], "serve": "on", "rolloutPct": 50 }
  ]
}

Rules are validated before they save

ShipOS validates rule and condition shape, operator/type mismatches, array-size limits, unknown fields, before writing. An invalid rule set is rejected, not stored, so you never push a broken config to the edge.

Rules compose with rollouts and the base config, see how a value is chosen.