Problem
Scoping a trigger to a deploy-time input today means hand-writing a magic string:
slack: [{ on: 'message.created', paths: ['/slack/channels/${SLACK_CHANNEL}/**'] }]
Three footguns, all hit in practice:
- Looks like a JS template literal but isn't. It's in single quotes, so
${SLACK_CHANNEL} is a literal the cloud substitutes at deploy (the #1999 interpolation), not JS. "Fixing" it to backticks makes JS interpolate it to undefined → silently broken. A quote character changes behavior.
- No link to the declared input. Nothing connects the string to
inputs: { SLACK_CHANNEL: … }. Typos surface as a deploy error at best, a silent miss at worst.
- Raw relayfile glob. Authors must know
/slack/channels/<id>/**, the /**-vs-mid-* rules, provider-root drop, etc.
Proposed authoring surface
input(name) — a branded deploy-time reference (returns DeployRef, not string; renders to ${NAME}):
import { defineAgent, input } from '@agentworkforce/runtime';
First-class provider scoping fields on triggers (headline):
slack: [{ on: 'message.created', channel: input('SLACK_CHANNEL') }]
github: [{ on: 'issues.opened', repo: input('REPO') }]
persona-kit expands channel → /slack/channels/<resolved>/**, repo → /github/repos/<resolved>/**. No glob, no /**, no ${}. Fields accept DeployRef | string (a literal id works too).
Escape hatch for raw paths (tagged template):
paths: [rfPath`/slack/channels/${input('SLACK_CHANNEL')}/**`]
rfPath substitutes the DeployRef → ${NAME} itself, so the backtick footgun is gone.
Types
declare const DEPLOY_REF: unique symbol;
export interface DeployRef {
readonly [DEPLOY_REF]: true;
readonly inputName: string;
toString(): string; // "${inputName}"
}
export function input(name: string): DeployRef;
export function rfPath(s: TemplateStringsArray, ...refs: (DeployRef | string)[]): string;
Type safety — compile-time validation
Inputs live in persona.ts, refs in agent.ts, so full editor types would mean threading the inputs type across files (invasive). Instead, validate at parse/compile:
- Collect every
DeployRef.inputName used in trigger fields/paths.
- Cross-check vs declared
persona.inputs.
- Error loudly on unknowns:
agent.ts references input 'SLAK_CHANNEL' not declared in persona.inputs (did you mean 'SLACK_CHANNEL'?).
Deterministic, pre-deploy. (A typed input<keyof Inputs> variant is a possible follow-up if a persona exports its input keys.)
Compile-down (no cloud change)
input('X').toString() → '${X}'.
channel/repo/rfPath expand to the existing paths: ['…${X}…'] form during parse.
- The cloud's existing
interpolateTriggerPathInputs (#1999, AgentWorkforce/cloud) resolves ${X} at deploy unchanged.
Back-compat
Raw paths: ['…${SLACK_CHANNEL}…'] keeps working; add a compile warning pointing at input()/channel:. No hard break.
Implementation (persona-kit + runtime only)
| File |
Change |
packages/persona-kit/src/input.ts (new) |
input(), DeployRef, rfPath |
packages/persona-kit/src/define.ts |
extend TypedTrigger<P> with provider fields (channel?/repo?: DeployRef | string) |
packages/persona-kit/src/parse.ts |
expand fields → paths; render refs → ${name}; collect referenced names |
packages/persona-kit/src/define.ts / compile |
cross-check referenced names vs declared inputs → loud error |
packages/runtime/src/index.ts |
re-export input, rfPath |
No AgentWorkforce/cloud change.
v1 scope / decisions
- Fields:
channel (slack) + repo (github) only; add others on demand.
- Validation: compile-time (above), not editor types.
- Deprecation: keep the string form + emit a compile warning.
Acceptance
input('X') + channel:/repo: + rfPath compile down to the existing ${X} token; deploy-time resolution unchanged.
- Unknown input ref → loud compile error (typo caught pre-deploy).
- Legacy
${} string form still resolves, with a deprecation warning.
- Tests cover: ref rendering, field→path expansion, validation error, back-compat.
Context
Motivated by the linear-slack channel-scoping work (cloud#1999/#2000/#2007) — the interpolation engine exists; this is purely the authoring DX on top of it.
Problem
Scoping a trigger to a deploy-time input today means hand-writing a magic string:
Three footguns, all hit in practice:
${SLACK_CHANNEL}is a literal the cloud substitutes at deploy (the #1999 interpolation), not JS. "Fixing" it to backticks makes JS interpolate it toundefined→ silently broken. A quote character changes behavior.inputs: { SLACK_CHANNEL: … }. Typos surface as a deploy error at best, a silent miss at worst./slack/channels/<id>/**, the/**-vs-mid-*rules, provider-root drop, etc.Proposed authoring surface
input(name)— a branded deploy-time reference (returnsDeployRef, notstring; renders to${NAME}):First-class provider scoping fields on triggers (headline):
persona-kit expands
channel→/slack/channels/<resolved>/**,repo→/github/repos/<resolved>/**. No glob, no/**, no${}. Fields acceptDeployRef | string(a literal id works too).Escape hatch for raw paths (tagged template):
rfPathsubstitutes theDeployRef→${NAME}itself, so the backtick footgun is gone.Types
Type safety — compile-time validation
Inputs live in
persona.ts, refs inagent.ts, so full editor types would mean threading the inputs type across files (invasive). Instead, validate at parse/compile:DeployRef.inputNameused in trigger fields/paths.persona.inputs.agent.ts references input 'SLAK_CHANNEL' not declared in persona.inputs (did you mean 'SLACK_CHANNEL'?).Deterministic, pre-deploy. (A typed
input<keyof Inputs>variant is a possible follow-up if a persona exports its input keys.)Compile-down (no cloud change)
input('X').toString()→'${X}'.channel/repo/rfPathexpand to the existingpaths: ['…${X}…']form during parse.interpolateTriggerPathInputs(#1999, AgentWorkforce/cloud) resolves${X}at deploy unchanged.Back-compat
Raw
paths: ['…${SLACK_CHANNEL}…']keeps working; add a compile warning pointing atinput()/channel:. No hard break.Implementation (persona-kit + runtime only)
packages/persona-kit/src/input.ts(new)input(),DeployRef,rfPathpackages/persona-kit/src/define.tsTypedTrigger<P>with provider fields (channel?/repo?:DeployRef | string)packages/persona-kit/src/parse.tspaths; render refs →${name}; collect referenced namespackages/persona-kit/src/define.ts/ compilepackages/runtime/src/index.tsinput,rfPathNo
AgentWorkforce/cloudchange.v1 scope / decisions
channel(slack) +repo(github) only; add others on demand.Acceptance
input('X')+channel:/repo:+rfPathcompile down to the existing${X}token; deploy-time resolution unchanged.${}string form still resolves, with a deprecation warning.Context
Motivated by the
linear-slackchannel-scoping work (cloud#1999/#2000/#2007) — the interpolation engine exists; this is purely the authoring DX on top of it.