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; omit to use the type's default
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)
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/SMTP3.0.1 recommended
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 list / cotctl bots versions <BotType> still work as deprecated aliases of the bot-types commands.
  • 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.