The workflow is green in the list, GET /api/v1/workflows/{id} returns "active": true, the Schedule Trigger holds a valid cron, there are no errors under Executions — and no executions either. A manual run works. Webhooks in the same workflow work. The schedule alone does not.
#What is actually going on
When a workflow is updated programmatically, the simplest route looks obvious: switch it off, push the new version, switch it back on. Roughly this:
curl -X POST "$BASE/api/v1/workflows/$ID/deactivate" -H "X-N8N-API-KEY: $KEY"
curl -X PUT "$BASE/api/v1/workflows/$ID" -H "X-N8N-API-KEY: $KEY" \
-H 'Content-Type: application/json' --data @workflow.json
curl -X POST "$BASE/api/v1/workflows/$ID/activate" -H "X-N8N-API-KEY: $KEY"All three requests return 200. The last one returns the workflow object with "active": true. As far as the API is concerned, the job is done.
But a schedule trigger in n8n is not a flag in the database, it is a registered job in the process’s scheduler. Activating through the public API sets the flag, but it does not always restore that registration. The state in the database and the state in the scheduler drift apart, and from the outside the gap shows up nowhere: not in the workflow list, not in the API response, not in the logs.
#How to update a workflow properly
Push the new version with a single PUT straight into the active workflow, switching nothing off:
curl -X PUT "$BASE/api/v1/workflows/$ID" \
-H "X-N8N-API-KEY: $KEY" \
-H 'Content-Type: application/json' \
--data @workflow.jsonHere is why this works. In the public API the active field is read-only — it is not part of the PUT body, and the request does not change it. So updating an active workflow does not reset its state: it stays active. The scheduler picks up the changed schedule on its own.
We checked this: pushed a new schedule into an active workflow and waited for it to fire at the appointed minute, with no deactivation.
#How to check that the schedule is registered
The active flag confirms nothing. The only confirmation is an execution that actually happened. The simplest check is to set the schedule to the next minute, wait for it, and look at the list of executions:
# put the next minute into the Schedule Trigger cron, push with PUT, wait,
# then check whether an execution of type trigger has appeared
curl -s "$BASE/api/v1/executions?workflowId=$ID&limit=5" \
-H "X-N8N-API-KEY: $KEY" \
| jq -r '.data[] | "\(.id) \(.mode) \(.status) \(.startedAt)"'The field to look at is mode. A manual run gives manual, a webhook gives webhook, and only the schedule gives trigger. If after the update the list holds nothing but manual, the schedule is not registered — however many times the API answered active: true.
#What to do if the schedule has already stopped working
The Active toggle in the interface restores the scheduler’s state: switch it off by hand and back on. That defeats the idea of pushing automatically, but as a one-off procedure it works. After that, move to PUT without deactivation and the problem will not come back.
Restarting the n8n process itself also restores the registration, because on start n8n reads the schedules from the database again. If you are on Docker and a restart is cheap, that is quicker than hunting for the right workflow in the list.
#Why PUT suits generated workflows
A PUT replaces the workflow whole, node ids included. If you generate the JSON with a script and the node ids are random every time, each push rebuilds the connections and shows changes where nothing changed.
To avoid that, the node id is made deterministic — a hash of the name. Then a repeat push does not break the connections and shows only the real changes. There is a separate piece on how that works: n8n workflows in code.
Together this gives you a working loop: edit in the generator → PUT → check the executions. No deactivation, and nothing done by hand in the interface.
#Takeaways
- A
deactivate+activatesequence through the public API returns200andactive: true, but the schedule can be left unregistered afterwards. - Update the active workflow with a single
PUT— theactivefield in the public API is read-only anyway. - Check for an execution with
mode: trigger, not for the flag. - The registration is restored by the Active toggle in the interface, or by restarting the process.
- Deterministic node ids make a repeat push safe.
#Common questions
#Can you update an active workflow through the API without deactivating it?
Yes. The active field in the public API is read-only, so a PUT does not change the workflow’s state. It stays active, and the scheduler picks up the new schedule value on its own.
#Why does the API return active: true when the schedule is not working?
The active flag is stored in the database, while the schedule is handled by a registered job in the process’s scheduler. Those are two different states, and after an activation through the API they can drift apart. The API response shows you only the first of them.
#What is the quickest way to check that the trigger is registered?
Set the schedule to the next minute, wait for it, and look at GET /api/v1/executions. If no execution with mode: trigger has appeared, the trigger is not registered.
#Sources
- n8n Public API — Workflow — the methods and fields, including the fact that
activecannot be edited through the API - n8n Docs — Schedule Trigger — how the schedule trigger works
- From our own experience