The previous pass was shorter but not clearer: it opened by announcing its own
importance, then leaned on "a run at 3am has nobody to ask" to carry the point.
Rewritten in the register the Storage section already uses — state the fact,
then the reason. The two cases are labelled rather than narrated, so the
structure is visible without prose holding it together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same three ideas, roughly half the words: a live request proves who is asking, a
3am run has nobody to ask, and a minted token has to be re-checked because it
only proves who was named when it was issued.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A feature description for people who did not follow the work: what the catalog
is for, the decision it rests on, how identity and scheduling actually work, and
where it is still thin.
Separate from the handoff notes, which are a working log. This one leads with
the problem — a workflow is shared, but the accounts it runs on are the
builder's, and a Schedule Trigger belongs to the workflow rather than to a
person.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A trigger set to "Accept all data" was excluded for declaring no contract. But
declaring nothing is not the same as being unofferable: the workflow simply
takes no input, which is exactly where a declared contract with no fields in it
lands — and that one was offered. So was a manual trigger, which carries less of
the builder's intent than any Execute Workflow Trigger does.
The line was in the wrong place. Three cases that all render an empty form and
start with `{}` now behave alike.
This also takes the pre-1.1 trigger with it: it has no mode to set, reads as
passthrough, and was excluded for the same wrong reason. A builder whose node
predates 1.1 sees no dropdown to fix and no explanation, so excluding it bought
nothing.
`own-schedule` and `no-start-node` remain, and both say something a builder can
act on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`findAllWhere` skips its per-user narrowing whenever the caller holds the scope
globally, so `findAllWorkflowsForUser(user, ['workflow:execute'])` returned every
`shared_workflow` row on the instance to anyone with global execute — an
instance owner, for one. Their catalog was every workflow there is, other
people's personal ones included, and the listing handed out those names.
Not a lie in RBAC terms — an owner may run any of them — but the catalog asks a
different question. It offers "the workflows you were given", and a global scope
is an administrative capability, not a grant of a particular workflow.
`findAllWorkflowsForUser` takes a `sharedWithUserOnly` option that keeps the
narrowing on regardless, and the catalog passes it. Default behaviour is
unchanged for every other caller. An owner still sees their own workflows —
membership of their personal project is what puts them there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The catalog's run endpoint never attached a credential context. `execution-
context.ts` only picks one up when `encryptedRunnerIdentity` is set, and nothing
set it, so a dynamic credential had no end user to resolve to and the workflow
quietly ran on whatever it stores itself — the opposite of the point.
It affected every run from the catalog, not any particular trigger. The
scheduled path had identity from the day it was written, because that one mints
a token; the attended path, where the identity was sitting right there in the
request, had none. The design note claiming manual runs take identity from the
session described an intention, not the code.
Now the controller reads the session token off the request and the run service
encrypts it, the same way the editor's manual execution has always done. A
minted identity still wins when one is supplied, so the unattended path is
unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running a workflow now and putting it on a schedule were held to the same bar,
and they should not be. Running once is bounded — the person is there, it
happens once, they see the result — and anyone with execute access could have
run it from the editor anyway, so a manual trigger is enough to justify it. A
schedule is unattended, recurring, and acts with that person's accounts for as
long as it stands.
That is too large a commitment to infer from the mere presence of a start node.
The Execute Workflow Trigger is the builder saying "this is meant to be called
by something other than me"; a manual trigger says the opposite. Subscriptions
now require the former, on create and on update — the trigger can be taken back
out after someone subscribed, and changing a schedule must not quietly re-bless
it.
Manual-trigger workflows stay in the catalog and stay runnable. Their schedule
button is disabled with the reason on it rather than hidden, so nobody is left
looking for an option that silently vanished.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing calls it: the catalog now sends people to the workflow's own execution
list, which shows the same runs with their data instead of a name, a time and a
status. Takes `CatalogService.listRuns`, its limit and its response types with
it, and drops the `ExecutionService` dependency that existed only for it.
The `catalogRunUserId` metadata the run service writes stays. It is the only
record of which person a run belongs to — a scheduled run has no session behind
it, so nothing else on the execution says whose it is — and unlike a query, it
cannot be reconstructed once it is not written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records the architectural decision the feature rests on — one workflow with an
Execute Workflow Trigger, not a generated wrapper per subscriber — the reasons
the wrapper approach was rejected, and what a live walkthrough confirmed and
found. Open decisions that need the scheduler owners are called out, as is the
missing offline OAuth refresh that blocks shipping schedules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two tables behind running a catalog workflow on your own schedule. They are
split because the keys differ: consent is one grant per (workflow, person),
while one person may want several schedules for the same workflow. The
subscription hangs off the grant by composite foreign key, so withdrawing
consent takes the schedules with it.
The scheduler side needed widening to hold them. A job was identified by
`(workflowId, nodeId)`, which is what provisioning diffs and deprovisioning
deletes by, and a per-person schedule has no node — several people hold their
own schedules for the same workflow. `scheduled_job` gains a nullable, opaque
`ownerId` that the scheduler only ever pairs with `taskType` and never
interprets, so a feature can own jobs without the scheduler learning about the
feature.
An owner-scoped job deliberately records no `workflowId`: that column is a
foreign key onto `workflow_published_version`, so it means "belongs to a
published trigger", which these do not. The type makes passing one impossible.
It also means nothing in the database removes these jobs when their owner goes,
so callers must deprovision explicitly.
The column is added with a raw `ALTER TABLE`: on SQLite the schema builder
rebuilds the table, and `scheduled_task` cascades from it, so the rebuild would
delete every queued occurrence on the instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>