Release notes
What changed in each published release of cotctl, newest first — with the migration steps you need when something breaks.
Check which version you're on:
cotctl --version
And upgrade to the latest one:
npm install -g @cotctl/cli@latest
Read the breaking changes before upgrading. Every release lists them first, and each one tells you exactly what to change in your YAML or in your pipeline. Releases with no breaking-changes section are safe to take as-is.
0.13.0 — 2026-10-02
A reliability release for survey validation. The headline is that a survey embedding another survey is no longer rejected because of how the embedded survey is named — the most visible of several checks that failed on YAML that was valid, or broke outright instead of reporting an error. One gap closed in the other direction: two fields that have to be lists are now checked as lists instead of being misread.
⚠ Breaking changes
A filters written as a map, and a jobTitle jobs written as a single value, now fail validation.
On a +property question filters has to be a list, and on a +person question with allow: jobTitle so does personFilter.jobs. A filter missing its leading - , or jobs: mgr instead of jobs: [mgr], used to pass the semantic check — validate exited 0, and so did surveys apply --dry-run --skip-remote-validation. The apply that followed then failed without naming the problem, or looked up one JobTitle per letter of the value. Both now report filters must be a list or jobs must be a list, name the question, and exit 1. Under any other allow, jobs is still not read and still not checked.
- Who this affects: anyone whose survey YAMLs write a
+propertyfilter without its leading-, or a single-valuejobsunderallow: jobTitle— and any survey that0.12.0already applied from one of those files. - What to do: start each filter with
-, and writejobsas a list. Then check the surveys0.12.0applied with a validation flag off: under--skip-remote-validationit wrote a single-valuejobsto the survey as a string, and under--skip-semantic-validationit wrote a missing one as an empty list. Those surveys export without their JobTitle codes, and re-applying that export fails withjobs must not be empty when allow is "jobTitle"— add the codes back as a list before you re-apply. - If you branch on exit codes: with
--skip-semantic-validationthe apply now refuses both before writing anything, and exits2—1underapply --dir.
Fixed
A survey that embeds another survey is no longer reported as missing because of how it is named.
surveys apply,apply(-for--dir,--dry-runincluded) andvalidate --remoteresolved each+surveyquestion'ssurveyCodewith a search the backend matches against the survey's name, not its code. A sub-survey named "Agregar Producto" with codeso_add_productcame back asSurvey with code "so_add_product" not foundand the command exited1— on valid YAML, an unchangedsurveys exportincluded. The only way past it was--skip-remote-validation, which switches off the survey's other remote checks along with it. The check now resolves codes the same wayapplydoes when it writes the reference, so the two agree on what exists: a code that no survey carries is still reported, with the same message, and a reference to an inactive survey passes.A
+surveyquestion whosesurveyCodeis missing, empty or not a string gets an error instead of breaking the command. AsurveyCodesuch as123or a list stoppedvalidateandapplyoutright; an empty one, or one carrying an accent or a slash, failed the apply with an HTTP 400 from the backend. They now get the usualsurvey requires a non-empty surveyCodeorSurvey with code "…" not foundand exit1.A survey that references more than ten PropertyTypes, JobTitles or Properties no longer fails on all but ten of them. Each kind of reference was looked up in a single request, which the backend answers with at most ten results. With eleven or more distinct codes of one kind — in
+propertyquestions, in the JobTitles of its+personquestions, or inpropertiesChannel/propertiesLimit— the rest came back as… not foundand the command exited1on valid YAML. Which codes failed depended on the backend's ordering rather than on your file, so the same reference could pass in one survey and fail in another. Each lookup now asks for every code it sends.A
+propertyquestion withoutfilters, or a+personquestion withoutpersonFilter, is reported instead of breaking the command. The apply stopped on an internal error that hidproperty requires at least 1 filterorperson requires personFilteralong with every other error it had already found. Those are now reported together, and the command exits1as it does for any other invalid survey.validate --remote --skip-semantic-validationbroke the same way; it now completes its remote checks.properties apply,workflows applyandjobtitles applyno longer fail with HTTP 431 when their YAML carries many long codes. They looked codes up in requests of up to 100 whatever their length, so enough long PropertyType or Property codes made a request larger than the backend accepts. They now split them the way the survey checks do.The help of
--skip-remote-validationand ofvalidate --remotenames what each one covers.--skip-remote-validationsaid "Skip remote identifier validation", but it skips the survey's remote checks of identifiers, of references to surveys, PropertyTypes, JobTitles and Properties, and of permission names. Three things still reach the API with the flag: a missing sub-survey and an unknown AccessRole inpermissionsstill stop the apply, because both are resolved when the survey is written, and a YAML that sets the survey'sidstill has itscodecompared with the server's.validate --remotesaid it checked identifiers; it also checks a survey's references to surveys, PropertyTypes, JobTitles and Properties.
Docs
The question-type reference now says that a +property question's filters and a jobTitle jobs have to be lists, and that a person question needs its personFilter; the troubleshooting page lists this release's new messages with their fix. The apply --dir page no longer says surveys are applied in reference order: they go in path order, so a child survey that does not exist yet has to sort first. The surveys page expands --code and adds that --search matches names.
Migration
- Run
validateover your YAML before you upgrade a CI gate. The two list rules are reported there now, and a file that breaks either one was already being misread on the next apply. - Re-check the surveys
0.12.0applied from a single-valuejobs. They export without their JobTitle codes; add the codes back as a list before re-applying. - Drop
--skip-remote-validationif you added it to get a sub-survey through. The falsenot foundit worked around is fixed, and the flag was switching off the survey's other remote checks with it.
0.12.0 — 2026-09-11
A consolidation release. The headline is that cotctl can now authenticate from the environment, so a pipeline no longer has to hand-write a profile file. Alongside it, two state-machine settings that could only be configured from the webclient became declarable from YAML — and, in the same pass, stopped being silently erased on every apply.
⚠ Breaking changes
validate --dir now fails on two things it used to let through.
A directory that exits 0 today can start exiting 1, for either of two reasons. The first is a survey's semantic errors — repeated identifiers, a reserved identifier, a dependsOn pointing at an identifier that does not exist. Those rules ran only at apply time until now. The second is a file:// reference that does not resolve, in any kind — a PropertyType carrying editable.src: "file://missing.js" passed as a plain string before and now fails the run.
- Who this affects: anyone running
validate --diras a CI gate over a directory of YAML. - What to do: run it once before you upgrade the pipeline, and fix what it reports. Nothing new is wrong with those files — the same failure was already waiting on the next
apply.
conditionalDisplay and command inside a +table column are now rejected.
Neither key was ever evaluated at column level: the client always renders every cell of a table. A YAML that validates and applies today stops doing both.
- Who this affects: anyone whose survey YAMLs put a condition on a table column.
- What to do: move the condition up to the
tablequestion itself, which is the level that is actually evaluated. Conditional display on a top-level question is unchanged.
surveys export now exits 2, not 1, when the simplified format cannot represent the survey.
This matches the contract every other input failure follows — 0 ok, 1 runtime failure, 2 invalid input. A transport or authentication failure still exits 1.
- Who this affects: only a pipeline that tests for
== 1specifically. One that treats any non-zero exit as a failure is unaffected. - What to do: if you branch on the exit code, add
2. And note this is a different2from the one above: a+tablecolumn refusal is a schema failure, which exits1fromvalidate,applyandsurveys applyalike.
surveys export no longer writes conditionalDisplay or command inside a column.
Since neither key does anything at that level, an export → edit → apply round trip was carrying a field with no effect.
- Who this affects: anyone who diffs exports against a file produced by an earlier version.
- What to do: expect the two keys to disappear from the next export. On the raw format this also removes an inert
commandvalue from the backend on the next apply, because a raw round trip replaces the question's content wholesale. Nothing observable changes, but the removal is real.
workflows apply --dry-run reports up to two more preserved fields per state machine.
Nothing about the apply changed. The two fields below stopped being wiped, and a preserved field is also a reported one, so the headline N preserved count grows.
- Who this affects: only a script that parses that number or diffs the dry-run output verbatim.
- What to do: re-baseline the expected output. There is no change in behaviour behind the new count.
Added
cotctlcan authenticate from the environment, with no profile on disk. Export an API token and the API URL of the environment it belongs to, then drop-c/--company:export COTCTL_TOKEN="$CI_COTCTL_TOKEN"
export COTCTL_API_URL="https://www.cotalker.com"
cotctl apply -f survey.yaml --yesUntil now a pipeline had to write
~/.cotctl/config.jsonby hand, reproducing an internal file format. The configuration is now built in memory and nothing is written to disk. The company is read from the token itself, so it cannot disagree with the credential, and one line goes tostderrnaming where the credential came from.Four things worth knowing before you wire it up:
-cstill wins. The variables are consulted only when-c/--companyis absent, so no existing invocation changes meaning ifCOTCTL_TOKENhappens to be exported.- It fails hard when the token is rejected. On a
401, or once the token has expired,cotctlstops and namesCOTCTL_TOKEN— it does not fall back to a password prompt, and it never writes your token into a profile behind your back. - A browser session token is refused. Only an API token is accepted, and the check costs no network call.
COTCTL_COMPANY_IDis optional and asserts which company the job expects. A token belonging to another one stops the run before anything happens, naming both ids.
cotctl login --tokenlogs in with no terminal attached. It takes a pre-generated API token as an argument instead of at a prompt, in three forms: the value itself, a file path (--token @<path>), or standard input (--token -).echo "$CI_COTCTL_TOKEN" | cotctl login --token -Prefer the indirect forms. A bare value sits in the process arguments, where any process on the same host can read it. Neither indirect form can hang:
--token -gives up after 10 seconds and stops reading at 8 KB, and--token @<path>warns — it does not fail — when the file is readable beyond its owner.Do not name that CI variable
COTCTL_TOKEN. That name is reserved as an environment credential and exporting it changes the behaviour of every later command that omits-c.A CI run also needs
--yes, and on most environments--allow-unverified-company: if the token's user cannot read the company record the check cannot conclude, and--yesdeliberately does not answer that one.A state machine can now declare the labels its task cards show. Until now
cardLabelscould only be configured from the webclient. The YAML takes a slot → PropertyType code map, so declaring a slot is what activates it:stateMachines:
- code: sm_po_main
cardLabels:
status1: pt_po_priority
status3: pt_po_supplierSlots may be sparse, and the card renders them in ascending slot order. Omitting the key preserves whatever the webclient configured; declaring the map deactivates every slot it leaves out, and
cardLabels: {}deactivates all of them —--dry-runnow says which labels the card stops showing. An unknown slot key is a validation error, andvalidate --dirresolves each slot against the PropertyTypes declared in the directory, so a typo is caught before the apply turns it into a400.A state machine can now pin the extensions its tasks accept. Same shape, same resolution by code:
stateMachines:
- code: sm_po_main
allowedExtensions:
- pt_po_photo
- pt_po_signatureAn absent key and an empty list are different instructions. Omitting the key preserves the server's list;
allowedExtensions: []declares an empty list and removes every extension the state machine accepted. The list is replaced wholesale rather than merged, so an omitted entry stops being an accepted extension, and--dry-runreports how many would be lost.defaultSelectedTaskTab— the tab a task opens on — now round-trips. The field was invisible tocotctlin both directions, so a workflow configured from the webclient lost the setting the moment anyone rebuilt its YAML from an export. Accepted values arenotes,channel,task,documentsandnull. There is nodetailtab — it has been reported as one, and adetailvalue is rejected rather than sent. An omitted key preserves the server value, an explicitnullclears it.Clearing a webhook's
contextis announced before it happens. An apply that would remove a populatedcontextnow names the slots being dropped and the consequence: the webhook stops being scoped and starts firing for every event of its trigger. Every path announces it — the warning channel on--dry-run, the confirmation preview on an interactive apply, and the warning channel again on an unattended one (-y, orapply --dir). Including under-q, which mutes only progress lines.Two configuration mistakes that used to apply cleanly and then do nothing now warn. A question declaring
dependsOn,showWhen,resetOnHideorresetIdentifiersat its root — those keys only take effect insideconditionalDisplay— and apreloadoronDisplaystage declaringsrcwithoutcontext, which is what registers the hook. Both are warnings, not errors: no exit code changes.applyaccepts-q, --quiet.surveys applyalready had it, so until now the warnings above could not be silenced on the path that applies a whole directory.schedules logsaccepts-v, --verbose, which prints the full stage output alongside the error.⚠
workflows exportoutput gains two fields.cardLabelsandallowedExtensionsare emitted only when they are actually configured, so a workflow that uses neither exports exactly as before — but if you keep exports under version control, the shape of the document changed and it will show up in a review.
Changed
webhooks apply -qnow mutes progress lines instead of warnings. The flag used to drop the warning channel entirely, which meantwebhooks apply -y -q— the canonical CI shape — could clear a webhook'scontextwithout printing a single line. It now suppresses the per-webhookWould CREATE/Would UPDATE/Created/Updatedlines; errors and destructive findings still surface. The flag, its short form and every exit code are unchanged, but a script that parsed those progress lines under-qwas reading output the help text never promised.
Fixed
workflows applywas silently erasing two state-machine settings on every run. A workflow's card labels and its list of allowed task extensions were sent as fixed empty values on every request, including updates — and since the backend replaces them wholesale, each apply cleared whatever the webclient had configured. It happened on workflows whose YAML mentions neither field, so an apply meant to change a name or a transition also erased configuration set from the UI, with nothing in the output saying so. Both now preserve the server value on update, and both became declarable from YAML — see the two entries in Added above.A
2xxresponse carrying no document no longer reads as success. Several endpoints answer200with an empty body for a request that did nothing — a patch against a record that was deleted, or one belonging to another company.cotctlreported the write as done and exited0, which in CI is indistinguishable from success. The writes now assert the document and fail naming the entity and its identifier. The same empty body on the read side used to read as a hit, which produced a string of user-visible failures now fixed:schedules applynever created a schedule — every apply took the update branch.surveys applyreported every free question identifier as already taken, blocking a legitimate apply.surveys applyrefused a rename that never happened, reporting the existing survey's code asundefined.jobtitles applyissued a patch against a nonexistent record. It now creates, as the miss implies.webhooks applyreportedupdatedfor a write that never happened.- A workflow apply could address an invalid task-group URL; it now fails naming the group.
One exception is deliberate: creating a schedule genuinely answers empty by design, so
schedules applystays silent there. Andbots applystill keeps its result when a patch answers empty — the Bot endpoints are documented to do that under some configurations — but it now warns onstderrnaming the bot, so you can tell a real echo apart from a fallback.A non-JSON response no longer surfaces as a parser error. Any response that was not well-formed JSON reached the operator with no status, no URL and no body. Two reported symptoms:
cotctl slas listagainst an environment where the route is not routed showedUnexpected token '<', "<html>...". It now reportsAPI Error 404: Not Found, the method and URL invoked, the content type received, and the first 200 characters of the body.cotctl workflows exportof a workflow with no state machines showedUnexpected end of JSON input. An empty successful body is now an empty result, and a404from the state-machine endpoints means there are none — the command warns and continues instead of aborting, and the YAML it writes omits the key rather than emitting it empty, so it re-applies without deleting anything.
A backend error message that is well-formed JSON still arrives unchanged.
The PropertyType report no longer claims a deletion nobody asked for was ignored. There are two cases and the message only distinguished one. A YAML that declares
schemaNodesand leaves some out is asking for a deletion, andcotctlstill refuses it withpreserved N schemaNodes not in YAML. A YAML that never mentions the section — the hand-written partial file that only touchesdisplayorisActive— asked for nothing, and now readskept N schemaNodes the YAML does not declare (no deletion was requested). Preservation itself is unchanged.The reference documentation for
schemaNodesalso stopped advertising a[]default, which implied that omitting the section and writingschemaNodes: []were equivalent. They are not, and that row is what led people to the message above.A machine identifier pinned at login is no longer silently replaced. The automatic re-login triggered when a token expires re-derived the identifier from the hostname and wrote it over whatever the profile held, so
cotctl login --machine-id ci-runner-3reverted on the first re-authentication — and the minted token's code changed with it. A pinned value is now reused; a hostname-derived one keeps being re-derived, so a renamed host is still picked up. Profiles written before this release are treated as derived — re-runcotctl login --machine-id <id>once to pin the value.webhooks apply:context: nullnow clears a webhook'scontext. Until now it could only be emptied through acontext: {}workaround. Omitting the key still preserves the server value, so no existing YAML applies differently.YAML Result contextomittedpreserved context: nullcleared (recommended) context: {}cleared (legacy, still supported) context: nullis accepted on every trigger, including the non-task ones that reject a populatedcontext— clearing a stale value is exactly what that validation asks you to do.applyno longer throws away every semantic warning. It kept only the entries marked as errors, sovalidatewas the one command in the CLI that ever showed a warning — andapplyis where the problem is actually suffered. Exit codes are unchanged: a warning stays a warning.validate --diraccepts a survey with externalexechooks again. Asrc: "file://./scripts/validate.js"reached the new semantic pass as literal text and was reported as a JavaScript syntax error. References are now resolved before validating, exactly asvalidate -fandapplyalready did.A deactivated PropertyType no longer aborts
workflows apply. A faster batched lookup introduced in this release filtered on active records only, so a workflow referencing a deactivated PropertyType — inpropertyType,asset.propertyType, a card-label slot orallowedExtensions— started failing withPropertyType "<code>" not found. Unresolved codes now fall back to an individual lookup, so the behaviour is back to what it was.A workflow apply resolves its PropertyTypes in one query. It used to ask for each code separately, per state machine: a workflow of 8 state machines with 4 distinct codes each made up to 32 requests where it now makes 1.
A refused
surveys exportnames the question. Several failure paths aborted the whole export with a message identifying neither the survey position nor the question. On a survey with sixty questions that is a message with nowhere to go. Each one now names the chat index and the identifier, and points at--format raw.--format rawis the supported escape hatch for a legacy survey, not just a debugging aid. It exports the survey verbatim and re-applies unchanged; it was previously mentioned only in passing.A simplified export that silently dropped content now warns. The worse failure was not the abort above but the successful export of an incomplete document: the YAML looks fine, gets committed, and the next apply is what removes the missing questions. Three cases now print to
stderrnaming what was left out, withstdoutuntouched so piping and-oare unchanged — a chat bubble of an unexpected content type, a legacy bubble packing several questions into one, and a standalone text question consumed as somebody else's label because of how its identifier is spelled.A taken question identifier now says which question took it. The message stopped at "it exists elsewhere", so you had to export in raw and dig the id out by hand. It now carries the id, the label, the type, whether the question is still active, and the export command to run. The owning survey itself cannot be named — the API offers no way to reverse that lookup.
schedules logsrenders the error the backend was already sending. A failed stage printed a date andexecutedand nothing else, while the detail sat unread in the response. The error now prints under the existing line whenever there is one. The current line and--jsonare unchanged, so a script parsing either keeps working.workflows applystops offering an escape hatch it rejects. When the permission catalogue did not answer, the message suggested retrying "or skip with--skip-remote-validation" — a survey-only flag that every path reaching a workflow refuses, so anyone following the suggestion got an error. It now says what actually works, and states that a workflow has no bypass flag.A workflow dry run says when it could not resolve the PropertyType codes. The lookup behind
cardLabelsandallowedExtensionsused to degrade to nothing and say so nowhere, so the diff reported no change for those fields — a dry run could look clean because the lookup had failed rather than because nothing had changed.A login that supplies its own token no longer reports having created one. With
--tokenor--paste-tokenthe success line readAPI token "…" createdand printed a revoke URL, for a token you did not mint and a panel you may not be able to reach. Both now reportacceptedand print no revoke line.-qsaid something false in seven commands, and now separates two classes of message. The help text forapply,bots apply,schedules applyandroutines applypromised to suppress non-fatal warnings without saying that a destructive finding is not one of them; onproperties apply,surveys applyandworkflows applyit announced "Only output errors" while a⚠ DANGERline still reachedstderrunder it. The flag's behaviour is unchanged — only the promise was false.Behaviour did change in one place:
-qnow really does silenceapply's Workflow warnings, which kept printing "Unknown bot type …" despite the flag. And the rule that holds everywhere:-qnever silences a destructive finding, on any kind. Progress lines and advisory warnings are muted; a finding that announces data being removed is not.The
--legacy-replacewarnings are in English, and no longer announce a removal that already passed.apply,workflows applyandsurveys applyprinted their escape-hatch warning in Spanish — the only non-English runtime text in the CLI — and promised the flag would disappear in a version this release is four and five minors past. Both flags are still present and behave identically; the text now states they are deprecated with no removal version announced.Smaller message fixes. A
subfilterValueerror named the wrong field and offered no way out; it now says the value is required unless the subfilter is*. Ajobtitles applythat falls back to creating because a record disappeared between listing and lookup now says so instead of doing it in silence. A workflow whose task group is missing now fails with a message naming it instead of a bare404. Andsurveys applyno longer prints its warnings in two different shapes — the mobile table warning now looks like every other warning.The assistant guidance bundled with the CLI was corrected on five points. These strings are compiled into the binary, so a wrong one cannot be fixed until the next release. It described two survey behaviours the CLI does not have; it taught a deprecated full-collection scan for job titles; it stated that every command requires a profile, which stopped being true with the environment credential above; it knew neither half of
schedules logs; and it presented avalidate --dir→apply --dirpipeline over twelve kinds whenvalidate --dirrecognises seven, so a directory holding routines, SLAs, schedules, bots or webhooks stopped at the validation step.
Known limitations
These are named, not fixed. They are worth knowing before you build a pipeline on them.
applynever prints the destructive-findings block. The⚠ DANGER/⚠ WARNINGblock a dry run shows undersurveys apply,workflows applyandproperties applyis not rendered by the genericapply— it computes the findings and discards them, and it declares no--fail-on-destructiveeither. For surveys, workflows and properties that block is the only place those findings surface, soapply --dir --dry-runis the quietest preview available: silent about exactly the changes that cannot be undone. Use the per-entity command when you want a preview that flags them. Webhooks are the exception — their findings also go through the warning channel, whichapplydoes print.schedules list --op failedcannot find a stage that failed inside the bot. The scheduler does not mark the run as failed in that case, so a health check built on that filter reports green through a schedule failing every night. The gap is in the backend, not in the CLI.schedules listaccepts a single-run filter it never applies. The flag is parsed and never forwarded, so the listing comes back unfiltered and exits0. Filter the--jsonoutput instead.-l, --limitonschedules listbounds what the backend returns, and the active filter runs afterwards, client-side — so a listing can come back shorter than the limit.
Migration
- Run
validate --dironce before you upgrade a CI gate. Two new classes of failure are reported there now, and both were already waiting on the next apply. - Re-export if you keep YAML under version control. Workflow exports gain two fields, survey exports drop two keys inside table columns, and neither change is a behaviour change — but both show up as a diff.
- Re-pin a machine identifier. A profile written before this release is treated as hostname-derived, so run
cotctl login --machine-id <id>once if you rely on the flag. - Check exit-code branches on
surveys export. A simplified-format refusal now exits2instead of1.
0.11.0 — 2026-09-01
⚠ Breaking changes
cotctl slas apply no longer forces pb.version: 'v3' when updating an SLA.
The default still applies when creating one. On update, a YAML that omits pb.version (or carries it as null / "") now leaves the server's value untouched instead of overwriting it with 'v3'.
Who this affects: anyone who relied on
applyto normalize SLAs created outsidecotctlup to V3. Those SLAs stay on whatever engine they have. If their bots use COTLang expressions ($VALUE#...,$INPUT#...) in the stagedata, those go unresolved under V2 — with no error.What to do: pin the version explicitly in the YAML —
pb:
version: "v3"
start: ...An explicit
pb.versionwas never touched byapply, on create or on update, and still is not.
A table question written in the simplified format is now validated exactly like the raw format.
The simplified schema previously skipped several checks the raw format always enforced — allowed column types, no nested tables, the column count and row limits, the column identifier charset, required column headers, and the per-column-type requirements. A survey YAML with a table question that passed validate before may now be refused.
Who this affects: anyone whose survey YAMLs carry
tablequestions written in the simplified format. The payloads now being refused were already failing, or silently misbehaving, on the server — the missing checks only hid that until now.What to do: re-run
validateover those files before your next apply —cotctl validate -f survey.yamlA new refusal means the YAML was relying on one of the gaps above, so fix it as the error describes rather than reading it as a regression.
Added
tablequestions now round-trip through the CLI.exportwrites a table'scolumns— previously dropped, so an exported YAML described a table with no columns — andapplyvalidates and applies them, in both the raw and the simplified YAML formats.A saved table and its columns are protected from destructive edits. Removing the table (or changing its identifier), removing a column (or changing its identifier), changing a saved column's type, and reordering, renaming or removing a saved option — each of these would silently orphan or re-label answers already stored, so all four are now refused, in
--dry-runas much as in a real apply. Renaming a table or a column is still fine — only the identifier is frozen — and so is adding a new column or appending new options.applywarns when a new table won't render on mobile yet. It works on web today; mobile support is coming in a later app release.--allow-unverified-companyonloginlets you continue when the environment can't confirm which company a token belongs to — see the company-verification fix below.cotctl login --url web.cotalker.com --subdomain acme --allow-unverified-company-y, --yesonloginoverwrites an existing profile without asking, and runningloginwithout a terminal now fails fast instead of hanging.cotctl login --url web.cotalker.com --subdomain acme --yesapplyreports the PropertyType schema fields it preserved, instead of merging them back silently:Updated: office_location (preserved 1 schemaNode not in YAML: 'city')and
--dry-runshows the same information before anything is sent. Deleting a schema field through YAML still isn't supported — useisActive: falseto retire one.These release notes are now generated automatically. Every
cotctlrelease opens a draft pull request against this page with the new section already drafted; it is reviewed and curated before it merges, so what you read here has been through a human pass.
Changed
cotctl login --urland--api-urlnow accept a host with no scheme.--url web.cotalker.comresolves tohttps://web.cotalker.com, and the command prints the URL it resolved. An explicithttp://is honored, for local and on-premise environments without TLS.
Fixed
login's API-URL autodiscovery no longer hangs, and explains what it tried when it fails. Every attempt is now bounded (6 seconds per candidate, 10 seconds for the whole sweep), thewww.↔web.sibling host is tried as a fallback, and a failure lists every URL it tried and why. It also stops picking up a commented-outapivalue from a whitelabel configuration file — which used to makelogindiscover, and authenticate against, the wrong host.loginrefuses to save a profile for the wrong company. The browser sign-in flow authenticates against whichever Cotalker session the browser already has, so signing in with another company's session open used to save a profile pointing at that company, silently.loginnow verifies the company after minting the token and before writing anything, and fails on a mismatch — naming both companies and how to fix it. When the company can't be verified at all,loginasks before saving;--allow-unverified-companyis how to proceed without a terminal. This is a behavior change for unattendedlogin: a scripted run against an environment that can't expose its company used to succeed with a warning; it now fails unless you pass the flag.- A cancelled profile overwrite now fails instead of reporting success. Answering anything but
yto the overwrite prompt used to exit0. It now exits1, on stderr. If a script only checks the exit code, this is a behavior change — a cancelled login now stops the pipeline instead of continuing with an unwritten profile, which is the intent. Profile savedis now verified against the file.loginre-reads the config after saving and confirms the profile is actually there before printing success, naming the exact path (Profile saved as "acme" to ~/.cotctl/config.json).surveys exportno longer overwrites one exec script with another. Two questions that produced the same extracted-script filename used to collide, and the second write silently won. Exported script files change name as a result of this fix — re-export rather than renaming existing files by hand.- Four
--helpdescriptions asserted things that were already false. Two flags claimed a removal that never happened, andproperty-types get --show-inactivepromised a filter it doesn't apply (inactive fields are already listed by default, tagged[INACTIVE]). No flag changed name or behavior — only what--helpsaid about them. surveys list --helpno longer advertises a-cshort form for--code.-cis already the global--companyflag, so the short form never reached--code— it silently searched for a profile with that name instead.--codeitself is unaffected.- The AI assistant's built-in guidance is corrected on two points it had gotten out of sync on: a workflow's
iconfield takes SVG path data, not an icon name, and a routine invoked from a bot stage takes its declared inputs as sibling keys ofcodein the samedatablock — omitting a required one never fails the apply, the routine just runs with that input empty.
Migration
A YAML exported by an older version has no columns block on its table questions, because export used to drop them. Re-applying such a file against a survey whose table already has saved columns now trips the "column exists but is missing from the YAML" refusal — once per column, since from the file's point of view every column disappeared. Re-export those surveys before re-applying them.
0.10.0 — 2026-08-21
⚠ Breaking changes
--allow-script-bots is now required on every apply path.
Applying a YAML that declares a stage of a script-executing bot type — PBScript, CCJS or ESMCode — now fails before anything is created or modified. Until this release the gate only existed on cotctl workflows apply; through the other paths such a bot applied silently and ran arbitrary JavaScript at runtime.
It now covers bots apply, slas apply, schedules apply, routines apply, and both apply -f and apply --dir — which previously had no way to pass the flag at all.
- What to do: re-run the same command with
--allow-script-botsto opt in explicitly. Pipelines that apply these bot types will start failing until you add it. The error message names each offending stage and its type. - Careful in CI: the refusal doesn't use one exit code.
bots applyandroutines applyexit 2;slas apply,schedules apply,workflows applyandapply -f/apply --direxit 1. If your script branches on the exit code, pin it to the command you actually invoke.
webhooks apply rejects a populated context outside the task trigger.
Scoping by survey / group / taskGroup only means something for create-edit-delete-task. On any other trigger the backend accepts it and then matches no event at all, so every delivery is dropped without a warning.
- What to do: change the trigger to
create-edit-delete-task, or removecontext. An emptycontext: {}is still accepted everywhere, so clearing a stale value keeps working.
A PBScript stage whose data.code isn't in your routine catalogue is rejected.
The code is now resolved against the live catalogue at apply time, with a suggestion when a close match exists. Before, the backend accepted any string and failed in production instead.
- What to do: fix the typo, or apply the routine first.
apply --diralready ordersRoutinebeforeSlaandSchedulefor exactly this reason.
Duplicate stage.key values inside one bot are rejected.
- What to do: give each stage a distinct key. Repeated keys made
stage.nextandbot.startambiguous and broke the stage-identity fix below.
permissionsV2 on a Survey is validated against the permission catalogue.
The field takes permission strings, never AccessRole names — passing a role name used to produce an opaque HTTP 500.
- What to do: replace role names with permission codes. Matching is case-sensitive. Use
--skip-remote-validationif the divergence is intentional.
Added
applyretries rate limits on its own. AnHTTP 429now backs off and retries up to 3 times, honouringRetry-Afterwhen the backend sends it. Largeapply --dirbatches no longer abort halfway and need a manual re-run.- Exit code
3for a partial apply. When a Workflow apply fails after creating some resources, it leaves orphaned records behind.apply --dirandworkflows apply -fnow exit3so CI can tell "needs manual cleanup" apart from an ordinary failure.cotctl apply -fdoesn't subscribe to the signal and still exits1— pin your CI branch to the command you invoke. validate --dirunderstandsJobTitleandUser. Both are now schema-checked offline instead of falling through as an unrecognized kind. Seven kinds are recognized — still fewer than the twelveapply --dirhandles, soRoutine,Sla,Schedule,BotandWebhookare not covered yet.--dry-runresolves references inside the same batch. A permission code, an AccessRole → JobTitle or a JobTitle → User reference defined in another document of the same run no longer reports a false failure.scaffoldaccepts state names as you type them.--states "Aprobada" "En compra"keeps the display name and derives thecodeslug from it; you no longer have to pre-slugify by hand and lose the label.- Non-blocking warnings for three documented data traps that previously only surfaced at runtime.
Fixed
- SLA state references resolve to the right id. An SLA whose
start.states[]/end.states[]used state codes resolved to the wrong record and was rejected withHTTP 500: SMStates not found. Codes now work as documented, and remain the recommended form. If you write a raw ObjectId there it must be theSMState._id. - Bot and SLA stages keep their identity across applies. Stage ids were reassigned on every apply, producing a permanent false diff in
--dry-runthat never converged. surveyssearch no longer returns the whole catalogue when the search term sanitises to an empty string.workflows applycan clearrequiredSurveyagain, and the dry-run preview now shows changes tonext[].canChange,next[].requiredSurveyandnext[].bots— they were being applied without appearing in the preview.- Multi-document files are dispatched per document. A YAML mixing several
kinds was previously dispatched by the first document's kind alone. - The bundled AI skills stated the wrong
apply --dirorder. Six of them had drifted and listedSurveyafterWorkflow— the inversion that orphans records when a state machine fails to create. If you author YAML with an assistant, this is worth re-reading.
Docs
Reference pages updated for subfilter / subfilterValue on properties, dataType[] when a stage invokes a routine, surveyTriggers preserve-vs-delete semantics, the validate exec-hook contract, and the transitions[].requiredSurvey clearing semantics.
Older releases. Versions before 0.10.0 are not documented here. If you're on one of them, upgrade to the latest and read the section above before your next apply.