Skip to content

Workflow Step Type

This module allows your app to register a custom workflow step type in a Crowdin Enterprise organization. Once the app is installed, the new step type appears in the workflow designer alongside the built-in steps (Translate, Proofread, TM Pre-translate, Custom Code, etc.). Project managers can add it to any advanced workflow, and at runtime every string that reaches the step is handed over to your app, which implements the step’s condition of done: it decides — asynchronously, via the API — when a string is done on the step and which output branch it leaves through.

Typical use cases: an external review or compliance gate, a scheduling/delay step, or any custom condition of done and routing logic that should run as part of the localization workflow.

Unlike UI modules, a workflow step is a long-lived participant in a project. The complete lifecycle looks like this:

  1. Install — an organization admin installs the app. Crowdin validates the manifest and creates a dedicated bot user (the Agent) for the app. The Apps SDK stores the agent credentials and handles the token exchange automatically.

  2. Design time — a project manager invites the Agent user as a manager to the project and adds your step to a workflow in the designer. Crowdin calls your app’s settings endpoint (the onStepSettingsSave function) with the step configuration.

  3. Runtime — when a string reaches your step, Crowdin sets its step status to NEED_PROCESS and sends the string.status_on_step.recalculation_triggered webhook to your app.

  4. Condition of done — your app evaluates its condition of done for the string (this can take arbitrarily long) and reports the result via the API by setting the string’s output port. Crowdin then routes the string into whatever step is connected to that output.

The platform handles routing, status bookkeeping, progress counters, and the editor; your app implements only the condition of done and routing logic. This is similar to the built-in Custom Code step, except the logic runs in your app instead of a script.

A workflow step needs an identity that can act inside projects on its own — read strings on the step, move them forward, and appear in the project history — independently of any human session. The crowdin_agent authentication type provides exactly that:

  • When the app is installed, Crowdin creates a dedicated bot user (the Agent) for it.
  • All of the app’s API calls authenticate as that user. Project access is governed by the Agent’s own project memberships — the Agent must be a project manager wherever the step is used. A project manager has to invite the Agent user as a manager to the project before your step can be used in its workflow.
  • Uninstalling the app removes the Agent user.

The Apps SDK takes care of the crowdin_agent token exchange for you: the client you receive in webhooks, module functions, and hooks is already authenticated as the Agent.

ParameterTypeRequiredDescription
usernamestringYesLogin for the Agent user. If it’s already taken, Crowdin appends a random suffix.
namestringNoDisplay name. The Agent appears as "{name} Agent". Defaults to the app name.
avatarUrlstringNoRelative URL to the Agent’s avatar, resolved against the app’s baseUrl. Defaults to the app logo.
const configuration = {
// ...
authenticationType: 'crowdin_agent',
agent: {
name: 'Reviewer',
username: 'sample-app-agent'
}
};

The example below registers a “Review Gate” step with two output branches. The webhook callback receives strings that arrive on the step and routes each one through an output port — replace this with your own condition of done (call an external system, check conditions, delay, etc.).

index.js
import crowdinModule from '@crowdin/app-project-module';
const app = crowdinModule.express();
const configuration = {
baseUrl: 'https://123.ngrok.io',
clientId: 'clientId',
clientSecret: 'clientSecret',
name: 'Sample App',
identifier: 'sample-app',
description: 'Sample App description',
dbFolder: import.meta.dirname,
imagePath: import.meta.dirname + '/logo.png',
authenticationType: 'crowdin_agent',
agent: {
name: 'Reviewer',
username: 'sample-app-agent'
},
workflowStepType: [
{
name: 'Review Gate',
description: 'Sends translated strings to an external review system',
boundaries: {
input: {
title: 'Translated',
ports: ['translated']
},
outputs: [
{
title: 'Approved',
port: 'approved'
},
{
title: 'Rejected',
port: 'untranslated'
}
],
},
settingsUiModule: {
formSchema: {
"title": "Workflow Step Settings",
"type": "object",
"required": ["reviewProfile"],
"properties": {
"reviewProfile": {
"type": "string",
"title": "Review profile",
"default": "default"
}
}
}
},
onStepSettingsSave: async ({ organizationId, projectId, stepId, workflowId, settings, context, client }) => {
await crowdinApp.saveMetadata({
id: `form-data-${organizationId}-${projectId}-${stepId}`,
metadata: settings,
crowdinId: String(organizationId),
});
},
onDeleteStep: async ({ organizationId, projectId, stepId, workflowId, context, client }) => {
await crowdinApp.deleteMetadata(`form-data-${organizationId}-${projectId}-${stepId}`);
}
}
],
webhooks: [
{
events: ['string.status_on_step.recalculation_triggered'],
async callback({ client, events }) {
for (const { stringStatus } of events) {
if (stringStatus.status !== 'NEED_PROCESS') {
continue;
}
// Evaluate your condition of done here and pick an output port
await client.workflowsApi.updateWorkflowStepStringStatus(
stringStatus.translation.project.id,
stringStatus.workflowStep.id,
stringStatus.affectedLanguage.id,
[{ op: 'replace', path: `/${stringStatus.translation.id}/output`, value: 'approved' }]
);
}
}
}
]
};
const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);
app.listen(3000, () => console.log('Crowdin app started'));
ParameterTypeRequiredDescription
keystringNoModule key. Defaults to {identifier}-{step_name_in_snake_case}.
namestringYesWorkflow step name shown in the workflow designer.
descriptionstringNoWorkflow step description.
imagePath/imageUrlstringNoStep logo shown in the workflow designer (a local file path or an external URL).
boundariesobjectYesDefines the input and output ports for the workflow step. See Boundaries.
editorModestringNoEditor mode for viewing strings. See Editor Mode.
settingsUiModuleobjectNoSettings UI module configuration. See Settings UI Module.
onStepSettingsSavefunctionNoCalled after saving a workflow that contains the step. See details below.
onDeleteStepfunctionNoCalled after the step is deleted from a workflow. See details below.

Defines the step’s connectors in the workflow graph:

  • input — exactly one input group: { title, ports }, where ports lists the string states the step accepts.
  • outputsone or two outputs (two is the maximum), each { title, port }.

Port and output titles must be 3–30 characters long.

Ports describe the state of a string’s content, not the step it came from. An output of one step connects to an input of another when their ports match, or when either side is all. Available port values:

PortMeaning
initialRaw string coming straight from the Start step. Input only — using it as an output fails the manifest validation during installation.
untranslatedString has no accepted translation.
translatedString has an accepted translation.
approvedString has been approved (proofread).
skippedString was bypassed (e.g. not sent to a vendor; applicable only for vendor blocks).
true / falseGeneric boolean branch pair (as used by Custom Code steps).
allWildcard — connects to anything on either side.

The conventional shape is one “success” output (translated, approved, or true) and one “failure/bypass” output (untranslated, skipped, or false).

Defines the editor mode for the workflow step. To allow viewing strings in the editor, this attribute should be defined. Read more about the editor modes.

Available values:

  • comfortable - Comfortable mode.
  • side-by-side - Side-by-side mode.
  • multilingual - Multilingual mode.

Object with the settings UI module configuration. This optional parameter allows you to define a UI for workflow step settings. The UI opens in an iframe when a project manager configures the step in the workflow designer, and the saved form data becomes the settings object your app receives in onStepSettingsSave.

You can use either a low-code approach with formSchema and formUiSchema, or provide your own custom UI with uiPath and fileName. Read more in the User Interface documentation.

Called when a project manager saves a workflow that contains your step (after adding or editing it). This is your registry of live steps — persist the received identifiers and settings so you know which steps exist and how they are configured.

If you don’t define this function, the SDK automatically stores the settings in the metadata storage under the key form-data-{crowdinId}-{projectId}-{stepId}. Define your own handlers if your step can be used in workflow templates — the default storage key includes projectId, which is absent for templates. Note that the TypeScript definitions currently declare both onStepSettingsSave and onDeleteStep as required, so TypeScript apps have to provide them either way.

  • organizationId - Crowdin organization ID.
  • projectId - Crowdin project ID. Not provided when the step is configured in a workflow template rather than a project workflow.
  • stepId - Workflow step ID.
  • workflowId - Workflow ID.
  • settings - Form data saved in the step settings UI.
  • context - Context object.
  • client - Crowdin API client (authenticated as the Agent).

Called when your step is deleted from a workflow, allowing you to clean up any per-step state. If you don’t define this function, the SDK removes the automatically stored settings.

Also treat app uninstallation as the deletion of all steps — Crowdin deactivates them and removes the Agent user.

  • organizationId - Crowdin organization ID.
  • projectId - Crowdin project ID. Not provided when the step is deleted from a workflow template.
  • stepId - Workflow step ID.
  • workflowId - Workflow ID.
  • context - Context object.
  • client - Crowdin API client (authenticated as the Agent).

When a string lands on your step (added to the project, moved there by an upstream step, or re-triggered), Crowdin sets its status on the step to NEED_PROCESS and sends the string.status_on_step.recalculation_triggered event. One event is generated per string per target language.

index.js
webhooks: [
{
events: ['string.status_on_step.recalculation_triggered'],
callback({client, events, webhookContext}) {
console.log('String status on step event:', events);
}
}
]

Learn more about webhooks configuration.

Webhook delivery is best-effort, not a reliable queue. Design your app accordingly:

  • Events are dispatched asynchronously — expect a delay of a minute or more after the trigger — and arrive batched (the events array can contain many events).
  • Delivery is retried only a limited number of times and only for transient errors. There is no durable retry queue.
  • If delivery fails, the affected strings are marked FAILED on the step and are not re-sent automatically. A project manager can re-trigger them from the project’s workflow step context (“trigger failed strings”).
  • Respond with a 2xx status quickly and process the events asynchronously — don’t do the actual work inside the request.
  • Because events can be missed, periodically reconcile using the API (see Processing Strings): list strings with status NEED_PROCESS on your steps and process any your app doesn’t know about.

The event is not sent when the recalculation was caused by your own status update (so your app doesn’t receive echoes of its own decisions), when the string was deleted, or when the string is already FAILED on the step.

Each event in the events array contains the following structure:

FieldTypeDescription
eventstringEvent name: string.status_on_step.recalculation_triggered
stringStatus.statusstringCurrent step status. See Status Values
stringStatus.outputstringOutput from the workflow step when the status is DONE
stringStatus.originEventstringAction that triggered the recalculation. See Origin Events
stringStatus.organizationIdstringCrowdin organization ID
stringStatus.translationobjectSource string details (id, key, text, file, project, etc.)
stringStatus.sourceLanguageobjectSource language information
stringStatus.affectedLanguageobjectTarget language affected by this step
stringStatus.workflowStepobjectWorkflow step that triggered the event
stringStatus.userobjectUser who triggered the action

Below is an example of string.status_on_step.recalculation_triggered event payload:

{
"event": "string.status_on_step.recalculation_triggered",
"stringStatus": {
"status": "NEED_PROCESS",
"output": "",
"originEvent": "string.added",
"organizationId": "200000001",
"translation": {
"id": 1000001,
15 collapsed lines
"identifier": "abc123def456",
"key": "unread_notifications",
"text": "{count} unread notifications",
"type": "text",
"context": "homepage",
"maxLength": "0",
"isHidden": false,
"isDuplicate": false,
"masterStringId": null,
"revision": 1,
"hasPlurals": true,
"plurals": {
"one": "1 unread notification",
"other": "{count} unread notifications"
},
"labelIds": [],
"url": "https://example.crowdin.com/editor/100/500/en-uk#1000001",
13 collapsed lines
"createdAt": "2026-01-21T12:29:56+00:00",
"updatedAt": null,
"file": {
"id": 500,
"name": "strings.xml",
"title": null,
"type": "android",
"path": "/strings.xml",
"status": "active",
"revision": "1",
"branch": {
"id": null
},
"directory": {
"id": null
},
19 collapsed lines
"project": null
},
"project": {
"id": 100,
"userId": 1,
"sourceLanguageId": "en",
"targetLanguageIds": ["uk", "de", "fr"],
"identifier": "abc123def456",
"name": "Example Project",
"createdAt": "2026-01-01T10:00:00+00:00",
"updatedAt": "2026-01-21T12:25:25+00:00",
"lastActivity": "2026-01-21T12:29:56+00:00",
"description": "Sample project description",
"url": "https://example.crowdin.com/u/projects/100",
"cname": null,
"languageAccessPolicy": null,
"visibility": null,
"publicDownloads": null,
"logo": "data:image/png;base64,iVBORw0KGg...",
"isExternal": false,
"externalType": null,
"hasCrowdsourcing": false,
"groupId": "10"
10 collapsed lines
}
},
"sourceLanguage": {
"id": "en",
"name": "English",
"editorCode": "en",
"twoLettersCode": "en",
"threeLettersCode": "eng",
"locale": "en-US",
"androidCode": "en-rUS",
"osxCode": "en.lproj",
"osxLocale": "en",
"textDirection": "ltr",
10 collapsed lines
"dialectOf": null
},
"affectedLanguage": {
"id": "uk",
"name": "Ukrainian",
"editorCode": "uk",
"twoLettersCode": "uk",
"threeLettersCode": "ukr",
"locale": "uk-UA",
"androidCode": "uk-rUA",
"osxCode": "uk.lproj",
"osxLocale": "uk",
"textDirection": "ltr",
7 collapsed lines
"dialectOf": null
},
"workflowStep": {
"id": 1000,
"title": "Review Gate",
"type": "Application",
"languages": ["uk", "de", "fr"],
"applicationModule": {
"applicationIdentifier": "sample-app",
"moduleKey": "sample-app-review_gate"
3 collapsed lines
}
},
"user": {
"id": "1",
"username": "john_doe",
"fullName": "John Doe",
"avatarUrl": "https://example.crowdin.com/avatar/1/small/avatar_default.png"
}
}
}

The stringStatus.status field indicates the current state of the string in the workflow step:

StatusDescription
NEED_PROCESSString needs to be handled by the application
TODOString is awaiting action (the app explicitly parked it)
DONEString was successfully handled and routed to an output
FAILEDWebhook delivery for this string failed; it must be re-triggered manually
INCOMPLETEString was previously available in the workflow step but is now missing

The stringStatus.originEvent field indicates what action triggered the workflow step recalculation. Possible values include:

String Events:

  • string.added - A new source string was added
  • string.updated - A source string was modified
  • string.deleted - A source string was deleted
  • string.restored - A deleted string was restored
  • string.triggered - A string was triggered in the workflow
  • string.triggered.after_file_update - A string was triggered after file update

Asset Events:

  • asset.added - A new asset was added
  • asset.triggered - An asset was triggered in the workflow
  • asset.triggered.after_file_update - An asset was triggered after file update

Duplicate String Events:

  • duplicate.string.triggered - A duplicate string was triggered in the workflow

Suggestion Events:

  • suggestion.added - A new translation suggestion was added
  • suggestion.updated - A translation suggestion was modified
  • suggestion.deleted - A translation suggestion was deleted
  • suggestion.restored - A deleted suggestion was restored
  • suggestion.approved - A suggestion was approved
  • suggestion.disapproved - A suggestion was disapproved
  • suggestion.voted - A vote was added to a suggestion
  • suggestion.voteCanceled - A vote was removed from a suggestion

File Events:

  • file.language_exclude - A language was excluded from a file
  • file.branch_protection.changed - Branch protection settings were changed

Your app moves strings through the step using two API endpoints, available on the Crowdin API client as client.workflowsApi. The client you receive in webhook callbacks and module functions is already authenticated as the Agent; both endpoints require the Agent to have manager access to the project (see Authentication).

Returns the statuses of strings on the step for a given target language. Use it to reconcile — for example, to find strings in NEED_PROCESS whose webhook your app missed.

const statuses = await client.workflowsApi.getWorkflowStepStringStatus(
projectId,
stepId,
languageId, // language code, e.g. 'uk'
{ limit: 500 }
);
// statuses.data[i].data => { stringId, languageId, stepId, status, output }

A maximum of 500 items is returned per request — use limit/offset to paginate.

Reports the result of your condition of done by setting the string’s output. The status is derived from the output — the app never sets a status directly:

  • "" (empty string) → the string is parked on the step as TODO (visible as pending work);
  • any of your step’s declared output ports → the string becomes DONE, and Crowdin immediately routes it into whatever step is connected to that output.
await client.workflowsApi.updateWorkflowStepStringStatus(
projectId,
stepId,
languageId,
[
{ op: 'replace', path: '/2814/output', value: 'approved' },
{ op: 'replace', path: '/2815/output', value: 'untranslated' }
]
);

Rules to keep in mind:

  • Only the replace operation is supported; the patch path is /{stringId}/output.
  • value must be one of the step’s declared boundaries.outputs[].port values, or "".
  • languageId must be one of the step’s target languages (see workflowStep.languages in the webhook payload).
  • Strings currently PENDING on the step can’t be patched; setting an unchanged value is a no-op.
  • There is no processing deadline: a string stays in NEED_PROCESS until your app reports a decision — your app owns liveness.
  • Crowdin Enterprise only — the step works only with advanced workflows. On crowdin.com the module is inactive.
  • crowdin_agent authentication is mandatory (see Authentication); the agent configuration object is required with it.
  • A companion webhooks module is required, subscribed to string.status_on_step.recalculation_triggered in the same app.
  • At most 2 outputs and exactly 1 input group; initial can be used only as an input port.
  • Webhook delivery is best-effort — see Delivery Guarantees. Reconcile via the API.
  • The Agent must have manager access to every project that uses the step. A project manager has to invite the Agent user as a manager; without this access, workflow publishing fails and API calls return 403.
  • Adding this module to an already-installed app requires re-approval by an organization admin.
SymptomCauseFix
Installation fails: Only crowdin_agent authentication type is allowed for workflow-step-type module typeThe manifest uses the default crowdin_app authentication type (or authorization_code/none)Set authenticationType: 'crowdin_agent' and add the agent object to the configuration
Installation fails: Requested scopes exceed the access level specified in the OAuth appThe scopes in the configuration are broader than the OAuth app’s scopesAlign the configured scopes with the OAuth app registration
Step type doesn’t appear in the workflow designerApp installed on crowdin.com (the module is Enterprise-only), or the webhooks module subscription is missingInstall on Crowdin Enterprise; add a webhooks module subscribed to string.status_on_step.recalculation_triggered
Workflow publish error: “…must use an agent authentication type”The app was previously installed with a different authentication typeReinstall the app with crowdin_agent authentication
Workflow publish error: “Step … requires manager permissions for …”The Agent user was not invited to the project or lost manager accessInvite the Agent user as a project manager
403 Forbidden on the string status APIThe request is not authenticated as the Agent, or the Agent is not a manager on the projectUse the client provided by the SDK; verify the Agent’s project role
404 on the string status APIlanguageId is not among the step’s target languages, or the step is not an active application stepUse the language codes from the webhook’s workflowStep.languages
400 when updating a string statusThe output value is not one of the step’s declared output portsSend one of the module’s boundaries.outputs[].port values or ""
Strings shown as “failed words” in the projectWebhook delivery to the app failedFix the app’s availability, then use the “trigger failed strings” action on the workflow step
Webhook never arrivesThe project uses a basic workflow; the workflow is delayed; the event name is misspelled; or the webhook belongs to a different app than the stepUse an advanced workflow; subscribe to exactly string.status_on_step.recalculation_triggered in the same app
Webhook arrives with a delayBy design: events are queued and dispatched asynchronouslyDesign the app for asynchronous processing