Skip to main content

Workflow bots

A bot is a unit of automation that runs inside a workflow. When a task changes state, or a survey is answered, the workflow engine executes the bots you wired into that transition — one after another — and routes to the next stage based on the branch each bot returns. Bots are how a workflow does things: create a task, send a message, call an external API, branch on a value.

This section is the human-readable catalog. Every bot Cotalker supports is grouped here by family, with a one-line description, a minimal YAML example, and the parameters you actually need day to day. For the exhaustive parameter list of any single bot, use cotctl bot-types versions <BotType> (see Checking versions) or search the RAG-backed reference.

How a bot is declared in YAML​

Bots live inside one of these slots of a workflow: requiredSurvey.bots, state.subtask.bots, state.next[i].bots, or state.surveyTriggers[i].bots. Each bot is a small state machine of its own — a list of stages, each naming a bot type and routing its outputs:

bots:
- name: my-bot # workflow-level name for this bot
start: s1 # first stage to run
stages:
- key: s1
name: PBUpdateTask # the bot TYPE (technical key)
version: "2.2.0" # optional; see "Checking versions" below
data: # type-specific parameters
_id: "{{task._id}}"
taskGroup: "{{task.taskGroup}}"
status1: "5d2fd97cb448357a12fe59ac"
next: # where to go for each output branch
SUCCESS: "" # "" ends this bot
ERROR: ""

Two rules that trip people up:

  • cotctl does not resolve ObjectIds inside stage.data. You must pass real ObjectIds. The only automatic resolutions are surveyTriggers[].survey (code → ObjectId) and AccessRole permissions by name. Inside data, a survey code in surveyId will not be resolved — pass the ObjectId.
  • Each bot declares its own output branches (SUCCESS/ERROR, IF/ELSE, FOUND/NOT-FOUND, DEFAULT, …). Declare in next every branch the bot can return, or the engine may not find a downstream stage.

Choosing the right bot​

Reach for a first-party bot before writing custom JavaScript. The table below maps a need to the recommended bot; the "Instead of" column is the common anti-pattern to avoid.

NeedUseInstead ofNotes
Create a taskPBCreateTaskCCJS + custom API callUse version 2.0.1
Update task fields (status, assignee, dates)PBUpdateTaskCCJS2.2.0 adds performance tracking
Advance the state machinePBChangeStatePBUpdateTask (does not record SM history)
Duplicate a taskPBDuplicateTaskmanual PBCreateTaskignoreChildren: true skips children
Send a channel messagePBMessagePBSendCustomNotification (that one is for push)Pin 2.2.0: the default never takes ERROR (error paths)
Message many channelsPBBulkMessageFCEach + PBMessage loop~90% fewer network calls
Send a surveyPBSendSurveyPBMessageeditMode: true for drafts
Reopen a sent surveyPBEditableSurveyPBSendSurvey with editMode
Copy a survey to another channelPBCopySurveyPBSendSurvey
Send an emailPBEmailNWRequest to SES/SMTPPin 3.0.1: the default never takes ERROR (error paths)
Push notification + channel messagePBSendCustomNotificationPBMessage
WhatsApp templatePBWhatsAppNWRequest to a BSPneeds a valid contractCode
Generate a PDFPBPdf (4.0.0)PBTemplate (HTML only)
Extract data from a PDFPBPDFExtractorCCJSAdobe Extract API (gated)
Render HTML from a templatePBTemplateCCJS string concatenation
Generate a QR codePBQRCodeNWRequest
Create a channelPBCreateChannelJPChannel
Patch a channelPBUpdateChannelJPChannel (RFC 6902)
Hide all channel messagesPBCleanChannelPBHideMessages (specific ids)
Add a user to a channelPBChannelAddUserJPChannel
Create / update a userPBCreateUser / PBUpdateUserNWRequest to /users
Patch a property granularlyPBUpdateProperty (3.0.0) or JPPropertyCCJSJP* is raw JSON Patch
Add editor/follower to a taskPBTaskAddEditorPBUpdateTask with editors[]additive; supports operation: remove
Iterate over an arrayFCEach (4.1.0)CCJS forEachone subcontext per item
Iterate large arrays in batchesFCEachBulkplain FCEachconfigurable batchSize/concurrency
Conditional branchFCIfElseCCJSeq/neq/gt/lt/gte/lte
Switch (first / all matches)FCSwitchOne / FCSwitchAllnested FCIfElse
Switch by a survey list answerPBSwitchListFCSwitchOnenext-keys match the question's code[]
Inline boolean gatePBConditionalFCIfElsereturns TRUE/FALSE
Pause the workflowFCSleepCCJS setTimeout
HTTP to an external systemNWRequest (2.0.0)CCJS with fetchsupports simulation mode
Check if an answer existsPBAnswerCheckerNWRequest to /answers
LLM query with MCP toolsPBLLMRunnerNWRequest to Vertex

If nothing fits, the escape hatches are the script-executing bots — see Network & code. They run arbitrary JavaScript and are blocked unless you apply with --allow-script-bots.

The bot families​

FamilyPageWhat lives there
Flow control (FC*)Flow controlLoops, branches, switches, sleep
Data extraction (JP*, ST*)Data extraction & patchingRaw JSON Patch and survey-answer string tools
Network & code (NW*, script bots)Network & codeHTTP requests and the gated JS escape hatches
Tasks & channels (PB*)Actions: tasks & channelsCreate/patch tasks, channels, editors, messages hiding
Messaging & surveys (PB*)Actions: messaging & surveysMessages, email, WhatsApp, surveys, buttons
Data & integrations (PB*)Actions: data & integrationsProperties, users, PDF, sheets, payments, LLM, calendars

What data a bot sees ($VALUE / data)​

A bot that looks identical in YAML behaves differently depending on which slot triggered it. The value it receives as $VALUE (exposed as data in the engine) is built by the engine from the trigger context. If your bot lives in an unusual slot, the safest move is to print $VALUE once with a PBMessage before referencing fields.

The two most common contexts:

requiredSurvey.bots — a state transition with a required form. When the user submits the form, the input is a WorkflowStart shape:

FieldOrigin
answerthe submitted answer document
taskthe task whose state is changing
meta.parentTaskparent task, if any
meta.taskGroupthe task's TaskGroup
data:
taskId: "{{task._id}}" # task is at the root of $VALUE
responderId: "{{answer.user}}"
taskGroupId: "{{meta.taskGroup}}"

state.surveyTriggers[i].bots — a survey answered inside a running state. Here the engine spreads the task at the root and puts the answer under sentAnswer:

FieldOrigin
_id, channel, taskGroup, status, …all task fields, spread at the root
sentAnswerthe answer that triggered the bot
data:
taskId: "{{_id}}" # the task itself (root), NOT task._id
taskChannel: "{{channel}}"
answerId: "{{sentAnswer._id}}"

Common mistakes in a surveyTriggers bot:

  • Referencing task._id — the task fields are at the root, so it is {{_id}}.
  • Referencing answer — the field is named sentAnswer there.
  • Assuming meta.taskGroup exists — it does not; use the task's taskGroup field directly.

Company-specific bots (CB*)​

A handful of bots (CB*) are loaded only for specific tenants and will fail anywhere else. The current tenants with custom bots are puertocoronel, muellesdepenco and primetime. Do not use them in cross-company workflows unless you have confirmed the bot is registered in the target environment.

BotTenantPurpose
CBCheckOTschedpuertocoronelTrigger the checkOT scheduler once (testing utility)
CBCoronelCreateMtNotificationpuertocoronelPush maintenance notifications to SAP via WS1 SOAP
CBGruaspuertocoronelPull crane registers into a PostgreSQL DB, split by shift
CBRocketReachIOGetContactprimetimeDespite the name, sets smState on a task (patchTask)
CBRocketReachIOSearchprimetimeStub — always returns SEARCH_ERROR (real call commented out)
CBErrorMessagesmuellesdepencoReturn channel messages whose content matches substrings
CBGetDeadTimeAnswermuellesdepencoLook up a dead-time / delay answer by composite id
CBValidateAssignationmuellesdepencoValidate a dead-time assignation equals the actual duration
CBValidateClosemuellesdepencoValidate no dead time ends after the shift's real end
CBValidateStartmuellesdepencoValidate the real start falls inside the planned window
CBValidateTMmuellesdepencoValidate a new dead time fits inside the planned shift

These bots depend on tenant-specific env vars, hard-coded survey ids and external services (SAP, PostgreSQL, SOAP). Treat them as internal integrations, not reusable building blocks, and confirm behavior with the owning team before designing a workflow around them.

Checking versions and the live catalog​

Most bots ship several versions, and the recommended one changes over time. The catalog is served by the backend, so query it live rather than trusting a static list:

cotctl bot-types list                    # full catalog: every type, its versions, and whether it has a default
cotctl bot-types versions PBUpdateTask # versions + default for one type

Notes:

  • cotctl bots versions <BotType> still works as a deprecated alias of cotctl bot-types versions. cotctl bots list is not an alias: it lists your Bot entities, not the catalog.
  • A stage.name that is not a known bot type produces a warning at apply time (typo detection), not a hard error — double-check the exact key when you see one.
  • A few bots have no default version and require an explicit version: — PBCalendar (2.0.0) and PBReport (1.0.0) are the notable ones.
  • On an update, an omitted version keeps the stored one (since 0.14.0 — it used to mean the type's default). Write version: null to send a stage back to its type's default. So an update may leave out the version of a type with no default when the stored stage pins one, and a kept version the catalog no longer registers only draws a warning.

Required data entries​

The live catalog also says which data entries each bot type and version requires — the ones the webclient won't save a stage without. Since 0.14.0, every apply checks each stage it writes — in a Workflow's bot slots, a Bot, a Routine, an SLA and a Schedule, with or without partial: true — and refuses with exit 2, --dry-run included, a stage that would be written without one:

… would be written without data.<key>, which its bot type requires; …
  • Empty means absent, null, "" or [] — an empty object {} counts as a value — and every element of a list must hold a value too: user: ["", "u1"] is refused as data.user[0].
  • It compares with the stored stage, paired by key. A stage is refused when the stored stage has the entry and the update drops it, and when nothing is stored — a new stage, one that moves to another bot type or version, or a list element the stored list lacks. A stage written over a stored one that already lacks the entry only draws a warning, so an export re-applies as it is over the stages it came from — except a Routine exported with 0.13.0 or earlier, which dropped every empty object: re-export it, or its PBScript stage is refused where the stored one holds data.data: {}.
  • Promoting between companies: an export applied to another company (QA → production) is checked against the stages stored there, so a stage that doesn't exist there yet is new, and every entry it lacks is refused.
  • PBScript needs data.data — the routine's input, and the only part of the stage's data that PBScript passes to the routine. Write data: {} when the routine takes no input. Every PBScript example cotctl shipped before 0.14.0 left it out, and an input written beside code never reached the routine.
  • A catalog that cannot be read skips the check, with one warning. cotctl validate -f --remote runs the same check on a Workflow's stages but only warns, since it has no stored stage to compare with.