Workflows

Table of contents


AddTaskCommentAction

Leaves an internal comment on the task the step received - not shown in the customer portal. Takes the task itself or, inside a for-each, the key it sits under (`model`), the way `update-task` does, and hands the task on.

Description

Leaves an internal comment on a task. The comment is not shown in the customer portal. Anyone a comment normally notifies (assignee, creator, assigner, users receiving all notifications) is notified as usual.

Config Example

{"model": "task", "comment": "<p>Ticket moved to the next phase</p>", "comment_category_id": 1, "comment_user_id": null}

model is only needed when the step receives the task inside an array, as the direct child of a for-each does. Hung on the true result of update-task, the step receives the task itself.

AssignContactUserAction

Adds a user to the contact's responsible users. Meant for rules like "whoever enters the potential becomes responsible for the customer": an `edit-contact` trigger, a `compare` on the field that changed, then this step with the user who saved (`###current_user###`, the default). It adds and never replaces - a contact already looked after by someone else keeps them, and a user already on the list is not added a second time, so the step can run on every save without piling up duplicate rows.

Description

Adds a user to the contact's responsible users (the "Verantwortliche" on the contact).

Meant for rules like "whoever enters the potential becomes responsible for the customer": run it after an edit-contact trigger and a compare on the field that changed. Without configuration it adds the user whose save ran the workflow.

NOTE: Adds, never replaces. A contact already looked after by someone else keeps them, and a user who is already responsible is not added a second time — the step can run on every save.

Configuration:

Returns the contact id, the user id and whether a new assignment was made.

Config Example

{}

Add a fixed user instead of the one who saved:

{"user_id": 4400}

"Whoever enters the potential becomes responsible" — the potential lives in field i_1 of each franchise partner's contact group, so one condition covers all of them:

  1. Trigger edit-contact
  2. compare — [{"action": "changed", "property": "custom_fields_*.i_1"}]
  3. assign-contact-user — {}

ChangeDataAction

Changes the return value by extracting a specific property from the data object.

Description

This action extracts a specific property from the input data object and sets it as the new return value.

Useful when you want to isolate and pass on a nested value instead of the full data object.

IMPORTANT: If the `target` property does not exist on the data object, this may result in a `null` or an error depending on context.

Returns only the value of data->{target}.

Config Example

[
  'target' => 'propertyName'
]

CheckFuelCardAction

Checks the fuel card photographed at an inspection against the vehicle, the way the fleet office does it by hand in the WUMS portal. The card names the vehicle it was issued for. When the AI reads that plate for certain, the check is recorded as the office would record it: the right card closes the check, a card of another vehicle is marked "wrong card" and left open for the office to tell the site. Anything less certain stays "not checked" for a person, with an internal comment saying why. A check a person has already recorded is never touched. Runs once per ticket: a second run finds the log entry and does nothing. A ticket whose photo has not arrived yet is left unmarked, so a later run can still check it.

Description

Reads the fuel card photo (the tenant's fuel card file name, e.g. *tankkarte.jpg) of a vehicle inspection ticket with the AI and compares the plate embossed on the card with the vehicle. A sure match records the fuel card check as correct and done; a sure plate of another vehicle records "wrong card" and leaves the check open. Anything uncertain stays "not checked" with an internal comment. Never overwrites a check a person recorded. Runs once per ticket.

Config Example

{"comment_category_id": 1, "comment_user_id": null}

Hang it next to check-pickerl, on the true result of the inspection compare, with the same queue_delay so the photos have arrived:

  1. Trigger new-task
  2. compare — [{"action": "equals", "property": "task_category_id", "value": 3}]
  3. notify-vehicle-inspection — {"queue_delay": 120}
  4. check-pickerl — {"queue_delay": 120, "comment_category_id": …}
  5. check-fuel-card — {"queue_delay": 120, "comment_category_id": …}

CheckPickerlAction

Checks the §57a sticker month a vehicle inspection carries against the photo of the sticker, and corrects it when the inspector typed it wrong. The month is typed by hand in the WUMS app and is often off — since September mostly the old year on a new sticker. The Pickerl report counts on it, so a wrong month hides an expired sticker. Nobody is meant to look at the result: a sure reading that differs replaces the entry, everything else leaves it alone, and an internal comment on the ticket says what happened. Runs once per ticket: a second run finds the log entry and does nothing. A ticket whose photo has not arrived yet is left unmarked, so a later run can still check it.

Description

Reads the §57a sticker photo (*serviceLabel.jpg) of a vehicle inspection ticket with the AI and corrects the entered sticker month (custom_fields_0.d_1) when the photo clearly shows a different one. Writes an internal comment when it corrects, adds, or cannot read the sticker. Runs once per ticket.

Config Example

{"comment_category_id": 1, "comment_user_id": null}

Hang it next to notify-vehicle-inspection, under the sleep step (both on its true result), so the photos have arrived:

  1. Trigger new-task
  2. compare — [{"action": "equals", "property": "task_category_id", "value": 3}]
  3. sleep — {"sleep_duration": 120, "queue_timeout": 300}
  4. notify-vehicle-inspection — {}
  5. check-pickerl — {"comment_category_id": …, "queue_timeout": 300}

CompareAction

Compares properties of a model against configured conditions.

Description

This workflow action evaluates one or more conditions against a model's properties.
Each condition can check for equality, inequality, string prefixes, or field changes.

You can also use dot-notation to access related model properties (e.g., user.name).
Useful for branching logic within workflows based on the current model state.

IMPORTANT: All conditions must pass for the action to return `true`. If any condition fails, it will short-circuit and return `false`.

Supported condition actions: equals, notEquals, startsWith, changed.

changed checks the trigger's changes rather than the record. On edit-contact these include the contact's custom fields, keyed custom_fields_<contact group>.<column>; a * in the property matches any run of characters, so custom_fields_*.i_1 is field i_1 in any contact group.

Config Example

[
  [
    'property' => 'status',
    'action'   => 'equals',
    'value'    => 'open'
  ],
  [
    'property' => 'user.name',
    'action'   => 'startsWith',
    'value'    => 'Jack'
  ]
];

CreateAction

Creates a new model instance using configuration key-value pairs.

Description

This workflow action creates a new record using the configured key-value pairs.
The target model class must be provided when initializing this action.

Each configuration value is processed through the placeholder system for dynamic replacements.
This allows template values like ###user_email### to be resolved at runtime.

IMPORTANT: Missing required fields for the target model may result in a database error.

Returns true if the object was successfully created, and tracks all saved attributes as changes.

Config Example

[
  'name' => 'Example Name',
  'email' => 'newuser@example.com',
  'role' => 'member'
];

CreateBsapiCustomerAction

Opens a debtor account in KFV's Sage accounting (through BSAPI) for a new contact, under the next number of Sage's own number range, and stores that number as the contact's bNR. The address is created first and the account added to it afterwards, since Sage wants the account to name its address. Each request and response is kept on the step. The bNR is only set once the account is open; when that last call fails, the address stays in Sage without one. Meant to run after a `new-contact` trigger. A contact that already has a customer number is left alone, so a second run never opens a second account. BSAPI is configured once for the whole installation, so the step refuses to run for any tenant but KFV.

Description

Opens a debtor account for the contact in KFV's Sage accounting (through BSAPI) and stores its number as the contact's customer number (bNR). The number is the next free one of KFV's debtor range in Sage (D000900364 and up).

Meant to run right after a new-contact trigger. Sends the person's name (or the company's, when there is no person), the e-mail and the birthday.

Three calls, in the order b2go prescribes: the address is created first (name, e-mail, birthday), then the next number is asked from Sage, then the debtor account is added to that address, naming the address number Sage handed out. Each request and response is kept on the step. The customer number is only stored once the account is open.

NOTE: KFV only. A contact that already has a customer number is skipped, so a second run never opens a second account.

Needs BSAPI_BASE_URL and BSAPI_TOKEN in the environment; without them the step fails.

Returns the contact id and the customer number. Follows the error branch when BSAPI rejects the address or returns it without its number, when Sage hands out no number, when BSAPI rejects the account, or when it cannot be reached. When only the account fails, the address stays in Sage without one.

Config Example

{}
  1. Trigger new-contact
  2. create-bsapi-customer — {}

EstimateDamageCostAction

Estimates a new body order in one call, with all its damage photos. The positions and the log entry are written together, so a failed call leaves nothing behind. A workflow step is never retried by the queue: the scheduled workflow in the config example runs this again for orders without a log.

Config Example

{ "queue_delay": 120, "queue_timeout": 900 }

Two workflows carry this action. A failed step is not retried by the queue, so the second one is what brings a failed or too early order back:

  1. On new-task: compare on the body category, then estimate-damage-cost as above.
  2. Scheduled every hour: fetch-list, for-each, estimate-damage-cost with { "queue_timeout": 900 }. An order that already has an estimate of this prompt version is skipped without a call.

fetch-list:

{
  "model": "Task",
  "select": ["id"],
  "where": [
    ["task_category_id", "=", 5],
    ["time", "raw", "time BETWEEN NOW() - INTERVAL 7 DAY AND NOW() - INTERVAL 1 HOUR"]
  ],
  "result_key": "orders"
}

for-each: { "loop_key": "orders", "result_key": "task" }

NotifyReportsToSignAction

The evening summary for the signers: one mail and web push per contact listing every report waiting for its signature, urgent ones marked, and the projects the contact was assigned to as inspector or deputy since the last summary. A contact with neither gets nothing.

Description

Sends each signer one summary of the reports waiting for their signature, by mail and web push. The signer of a report is the contact of its calendar event. Urgent reports come first and are marked "Dringend".

The summary also lists the projects the contact was assigned to under inspector_role_id or deputy_role_id since the last summary, each linking to the project in the app. An assignment counts as told once a summary listed it (table project_contact_notices). With the tenant setting note_own_project_assignments the one a contact made themselves, by creating or copying a project through the portal, is noted right away and never listed. A contact with neither reports to sign nor new projects gets nothing.

Meant for a time schedule trigger, e.g. every day at 19:00.

app_url is the app the signers work in: the mail and the push link to its report list on the "to sign" status, or to its project list when there are only new projects. damage_status_id is that status; without it the tenant setting damage_status_to_sign_id is used. Without inspector_role_id and deputy_role_id no projects are listed. logo_url replaces the logo in the header of the tenant's mail template, so the mail shows the app's logo instead of 1Tool's. header_color (#rrggbb) paints that header in the app's color: it replaces the color of the logo's cell wherever the template uses it.

Returns the number of reports per notified contact id (notified) and the number of new projects per contact id told of them (new_projects).

Config Example

{
  "app_url": "https://kfv-portal-pruefer.1tool.com",
  "logo_url": "https://kfv-portal-pruefer.1tool.com/Logo3.png",
  "header_color": "#1c4996",
  "inspector_role_id": 4,
  "deputy_role_id": 6
}

NotifyUrgentReportToSignAction

Tells the signer of an urgent report right away, by mail and web push: once the reviewer releases a report marked urgent for signing, or marks a report waiting for its signature urgent.

Description

Tells the signer of an urgent report right away, by mail and web push, instead of only in the evening summary of notify-reports-to-sign.

Meant for the edit damage trigger. It notifies when the edit marks a report waiting for its signature as urgent, or releases an urgent report for signing. Any other edit of the damage stays quiet.

Takes the same app_url, damage_status_id, logo_url and header_color as notify-reports-to-sign.

Config Example

{
  "app_url": "https://kfv-portal-pruefer.1tool.com",
  "logo_url": "https://kfv-portal-pruefer.1tool.com/Logo3.png",
  "header_color": "#1c4996"
}

NotifyVehicleInspectionAction

Mails the feedback on a vehicle inspection: the photos and the damages the inspector recorded, to the contacts on the ticket. The WUMS app used to ask for this mail itself, after its last photo upload — and stopped asking in 3.0.0 for every contact without a main contact, which in km_post is every one. Sending belongs to the server anyway: the ticket and its photos are here, and a workflow decides when (a sleep step after the ticket was created gives the uploads time to land). Sent once per ticket: a second run — the app's own request, a retried job, a workflow triggered twice — finds the log entry and does nothing.

Description

Sends the feedback on a vehicle inspection (WUMS): the inspection photos and the recorded damages, mailed to the contact that raised the ticket, the workshop it is addressed to, and the raising contact's main contact if it has one.

Meant to run a step or two after a new task trigger, behind a sleep step: the app creates the ticket first and uploads the photos in the background, so the mail has to wait for them.

NOTE: Sent once per ticket. A second run — the app's own request, a retried job — finds the mark in the workflow log and does nothing.

Takes no configuration. Returns the ticket id, the notified contact ids and the number of photos and damages.

Config Example

{}

A typical workflow for a fleet tenant:

  1. Trigger new-task
  2. compare — [{"action": "equals", "property": "task_category_id", "value": 3}]
  3. sleep — {"sleep_duration": 120, "queue_timeout": 300}
  4. notify-vehicle-inspection — {}

SendEmailAction

With this Workflow you can send out emails

Description

This action sends an email using 1tool-style placeholders (starting with three hashes: ###). It uses the currently logged-in user's email address as the sender.

IMPORTANT: If any of the required fields (to, subject, text) are missing, the email will be skipped and not sent.

Attachments: Set attachments_key to a relationship name or dotted data path on the workflow's context model; the file(s) found there are attached to the email. In a proposal context, attachments_key: "files" attaches the proposal's files (Proposal::files). The resolved value must be a File / Attachable (or a collection/array of them). Note the key is attachments_key, not attachments. Similarly, to, cc, and bcc are resolved against the context model when their value matches a property/path, otherwise used verbatim.

Returns: A boolean: true if the email was sent successfully, false otherwise.

Config Example

[
  'to' => 'Recipient email address',
  'subject' => 'Email subject line',
  'text' => 'Main content/body of the email',
  'cc' => 'Optional CC address (or data path on the context model)',
  'bcc' => 'Optional BCC address (or data path on the context model)',
  'attachments_key' => 'files', // Optional: relationship/data path on the context model; its File(s) are attached
  'email_setting_id' => 'Optional EmailSetting id to send through',
];

SendHttpRequestAction

Dispatches a custom HTTP request.

Description

This action sends an HTTP request using the provided configuration options.

Useful when you want to trigger external APIs or webhooks as part of a workflow step, passing in custom headers, body content, and query parameters.

IMPORTANT: The `url` field is required. If missing, the request will not be sent and the action will return `false`.

The HTTP response (status, headers, and body) will be stored in $this->returnValue for downstream use.

Supports:

Config Example

[
  'url' => 'https://api.example.com/send',
  'method' => 'POST',
  'headers' => [
    'Authorization' => 'Bearer secret_token',
    'Content-Type' => 'application/json',
  ],
  'body' => [
    'id' => 'data.id',
    'email' => 'data.email',
  ],
  'query' => [
    'debug' => 'true'
  ]
]

SendInvoiceAction

Dispatches an invoice job for a user.

Description

This workflow action dispatches an invoice generation job for the specified user.

It uses the given userId and the current data->id (usually the invoice ID) to trigger the process.

The job runs asynchronously using Laravel's job queue system.

IMPORTANT: If `userId` is missing from the configuration, the action is skipped and **no invoice will be sent**.

Returns a boolean: true if the job was successfully dispatched, false if the config was invalid.

Config Example

[
  'userId' => 'ID of the user for whom the invoice should be dispatched',
];

SleepAction

Pauses the workflow execution for a specified number of seconds.

Description

This workflow action delays execution for a number of seconds specified in the configuration.

It is useful for debugging, simulating delays, or testing time-based behavior in workflows.

NOTE: The delay is applied using PHP’s `sleep()` function. Long delays can block execution.

Returns the original data unchanged after the sleep duration.

Config Example

[
  'sleep_duration' => 5
]

TranslateProposalAction

Generates a translated PDF for a proposal and stores it as a file.

TranslateProposalPlaceholdersAction

Generates a translated PDF for a proposal by translating only the values substituted into the ###TOKEN### placeholders of the target proposal option's pdf_template, instead of translating the whole rendered HTML.

UpdateAction

Dynamically updates model fields using key-value configuration.

Description

This workflow action updates fields on the given model using key-value pairs from the configuration.

Nested relationships can be updated by using dot notation (e.g., profile.bio).

Each value is processed through the configuration replacement system to support dynamic inputs.

IMPORTANT: Invalid relationships or keys that do not exist on the model may result in errors or be ignored.

Returns the updated model and tracks changed values internally.

Config Example

[
  'name' => 'New name value',
  'email' => 'new@email.com',
  'profile.bio' => 'Updated biography text'
]

UseChatGptAction

Sends a query to ChatGPT and returns the response.

Description

This action sends a message to OpenAI's ChatGPT API and returns the AI-generated response.

Useful when you want to automate replies, transform content, or generate text using AI within a workflow.

IMPORTANT: You must provide a valid OpenAI API key and a user `query`. If the request fails or the response is invalid, the action will return `false`.

The action includes a preset system instruction to ensure ChatGPT behaves like a task executor:

"You're an assistant. Your role is to execute tasks exactly as requested, returning only the pure result without any suggestions, explanations, or additional help. Strip everything down to just what is asked—nothing more, nothing less."

The raw response is stored in $this->returnValue['content'].

Config Example

[
  'api_key' => 'sk-xxxx',
  'query' => 'Translate this text to German: Hello, how are you?',
  'model' => 'gpt-4o' // Optional, defaults to 'gpt-4o'
]

Back to top