Mode Router
Selects the active profile from triggering context. The piece that ties everything together: gates apply profiles, gates need to know which profile is active, the router decides.
The premise: profile selection is a routing problem with deterministic precedence, not a reasoning problem. Operational modes (e.g., active-supervision, mobile-only HITL, unattended) have well-defined boundaries — calendar entries, time windows, geofences, manual override commands — that a rule engine can evaluate without an LLM. The router applies those rules and returns a profile name. No interpretation, no judgment calls.
This Skill is the rootnode runtime layer's analog to a "context dispatcher" — the orchestrator's first call when work arrives, before the gates fire. Without it, every gate invocation requires the caller to specify which profile applies. With it, the orchestrator picks a profile based on what the world looks like at that moment.
Important
Configuration-driven, not hardcoded. The Skill itself contains no user-specific rules. The rule set lives in a router config file (validated against the included schema). An example router config (configs/example-router.json) ships as a working reference; users author their own.
Deterministic precedence. Manual override always wins over time-based rules. Time-based rules apply in a documented priority order. Default profile is the fallback when no rule matches. The Skill never picks "the most plausible profile" — it follows the rules in order.
Required default profile. Every router config must specify a default. This guarantees the router always returns a valid profile name. Halt-with-error on no-match is forbidden — autonomous orchestrators cannot recover from a routing failure.
The router doesn't validate that the returned profile exists. Profile existence is the consuming Skill's concern (the gate checks its own profile directory). The router returns a name; the consumer either has it or surfaces "profile not found." Single Responsibility Principle.
Time-zone awareness is the caller's responsibility. The router accepts a current-time input as ISO-8601 with timezone offset. The caller (UCIS, OpenClaw, etc.) is responsible for passing the right timezone. The router doesn't infer location.
Trigger Sources — Overview
The router supports five trigger source types. A router config can use any combination. Per-type configuration semantics, supported predicates, evaluation behavior, edge cases, and worked examples are in references/trigger-types-detailed.md.
1. Manual Override
A user-provided profile name that wins over all other rules. Source: Telegram command, Apple Shortcut, CLI flag. Has a TTL; after expiry, rules-based routing resumes. Always evaluated first.
2. Calendar State
The router evaluates the user's calendar at decision time. Supports three predicates: event_in_progress(category), event_starts_within(category, minutes), no_events_for(minutes). The router doesn't fetch calendar data — the caller provides active_calendar_events in context.
3. Time Window
Day-of-week + time-of-day windows. Configurable time zone per rule. Supports wrapping windows (start_time > end_time, e.g., 22:00-06:00). Inclusive of start, exclusive of end.
4. Geofence
Caller-supplied geofence state as a label (e.g., at_office, at_home, in_transit). The router doesn't compute geofences — it accepts a label. Match is exact string equality.
5. Custom Signal
User-defined named signals passed by the caller (battery level, network state, "do not disturb" toggle, etc.). Match types include equals, not_equals, truthy, falsy. The escape hatch for triggers the schema doesn't anticipate.
Compound Triggers
Multiple trigger conditions combined with AND, OR, or NOT logic. The most common trigger structure in well-designed router configs — express real intent ("weekdays during work hours AND at the office") rather than single-axis conditions. Full semantics including evaluation order, short-circuiting behavior, nesting, common patterns, and anti-patterns are in references/compound-trigger-semantics.md.
Workflow
When invoked:
Step 1 — Receive inputs. The Skill expects:
router_config: A router config object conforming to the schema. Specifies rules, default profile, manual override state.context: Current state — current time, calendar state, geofence label, custom signals. Caller is responsible for providing accurate context.
Optional:
verbose: When true, return the full rule evaluation trace in the output. Default false (just the verdict).
Step 2 — Check manual override. If router_config.manual_override is set AND the override's TTL has not expired, return the override's profile name immediately. Skip rule evaluation.
Step 3 — Evaluate rules in order. Walk the rule list in declared priority order (lowest priority number first). For each rule:
- Evaluate the trigger condition against
context - If the condition matches, return the rule's
profilename - If not, continue to next rule
Step 4 — Fall back to default. If no rule matched, return router_config.default_profile.
Step 5 — Generate verdict. Produce structured JSON output (see Output Format below). Include the selected profile, the trigger that matched (or default_fallback), and optionally the full evaluation trace.
Step 6 — Return. Output the JSON. The consuming Skill (handoff-gate, critic-gate, etc.) reads selected_profile and proceeds.
Output Format
{
"selected_profile": "balanced",
"matched_trigger": {
"type": "time_window",
"rule_id": "weekday-work-hours",
"description": "Weekdays 9-5 → day-job profile"
},
"context_snapshot": {
"current_time": "2026-05-01T14:30:00-07:00",
"geofence": "at_office",
"active_calendar_events": [],
"manual_override": null
},
"router_config_version": "1.0.0",
"evaluated_at": "2026-05-01T14:30:00-07:00",
"evaluation_trace": []
}
When manual override is active:
{
"selected_profile": "lenient",
"matched_trigger": {
"type": "manual_override",
"set_at": "2026-05-01T14:00:00-07:00",
"expires_at": "2026-05-01T15:00:00-07:00",
"set_by": "telegram_command"
},
"context_snapshot": { "...": "..." }
}
When fallback to default:
{
"selected_profile": "lenient",
"matched_trigger": {
"type": "default_fallback",
"reason": "no rules matched"
},
"context_snapshot": { "...": "..." }
}
When verbose: true, evaluation_trace contains an entry for each rule evaluated:
"evaluation_trace": [
{
"rule_id": "sleeping-window",
"evaluated": true,
"matched": false,
"reason": "current_time 14:30 is outside sleeping window (22:00-06:00)"
},
{
"rule_id": "weekday-work-hours",
"evaluated": true,
"matched": true,
"reason": "current_time 14:30 falls in weekday window (Mon-Fri 09:00-17:00) AND geofence is at_office"
}
]
Examples
Three worked examples — standard weekday afternoon at office (compound trigger match), Saturday morning with manual override (override pre-empts rules), and late evening with no rule match (default fallback) — are in references/trigger-types-detailed.md (in the "Worked Examples" section). A pre-existing routing walkthrough is also in examples/routing-walkthrough.md.
Router Config Structure
The router config conforms to schema/router-config.schema.json. Minimum structure:
{
"schema_version": "1.0.0",
"name": "example-router",
"description": "Example routing rules for three operational modes (lenient/balanced/strict)",
"default_profile": "lenient",
"rules": [
{
"rule_id": "sleeping-window",
"priority": 1,
"trigger": {
"type": "time_window",
"time_zone": "America/Phoenix",
"days_of_week": ["all"],
"start_time": "22:00",