# Workflow Step Type

This module allows you to add custom workflow step.

## Sample

```js ins={14-60} title="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',
      workflowStepType: [
        {
          name: 'Translation Delay',
          description: 'Delays strings until the file is fully translated',
          boundaries: {
            input: {
              title: 'Strings',
              ports: ['translated']
            },
            outputs: [
              {
                title: 'Strings',
                port: 'translated'
              }
            ],
          },
          settingsUiModule: {
            formSchema: {
              "title": "Workflow Step Settings",
              "type": "object",
              "required": ["delayHours"],
              "properties": {
                "delayHours": {
                  "type": "number",
                  "title": "Delay (hours)",
                  "default": 24
                }
              }
            },
            formUiSchema: {
              "ui:submitButtonOptions": {
                "submitText": "Save Settings"
              }
            }
          },
          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}`);
          }
        }
      ]
    };

    const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);

    app.listen(3000, () =>  console.log('Crowdin app started'));
    ```
  ```ts ins={15-61} title="index.ts"
    import crowdinModule from '@crowdin/app-project-module';
    import type { ClientConfig } from '@crowdin/app-project-module';

    const app = crowdinModule.express();

    const configuration: ClientConfig = {
      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',
      workflowStepType: [
        {
          name: 'Translation Delay',
          description: 'Delays strings until the file is fully translated',
          boundaries: {
            input: {
              title: 'Strings',
              ports: ['translated']
            },
            outputs: [
              {
                title: 'Strings',
                port: 'translated'
              }
            ],
          },
          settingsUiModule: {
            formSchema: {
              "title": "Workflow Step Settings",
              "type": "object",
              "required": ["delayHours"],
              "properties": {
                "delayHours": {
                  "type": "number",
                  "title": "Delay (hours)",
                  "default": 24
                }
              }
            },
            formUiSchema: {
              "ui:submitButtonOptions": {
                "submitText": "Save Settings"
              }
            }
          },
          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}`);
          }
        }
      ]
    };

    const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);

    app.listen(3000, () =>  console.log('Crowdin app started'));
    ```
  ## Configuration

| Parameter            | Type       | Required | Description                                                                                  |
|----------------------|------------|----------|----------------------------------------------------------------------------------------------|
| `name`               | `string`   | Yes      | Workflow step name.                                                                          |
| `description`        | `string`   | No       | Workflow step description.                                                                   |
| `boundaries`         | `object`   | Yes      | Defines the input and output ports for the workflow step. See [Boundaries](#boundaries).     |
| `editorMode`         | `string`   | No       | Editor mode for viewing strings. See [Editor Mode](#editor-mode).                            |
| `settingsUiModule`   | `object`   | No       | Settings UI module configuration. See [Settings UI Module](#settings-ui-module).             |
| `onStepSettingsSave` | `function` | No       | Called after saving workflow for custom data management. See details below.                  |
| `onDeleteStep`       | `function` | No       | Called after deleting a step for custom data management. See details below.                  |

### Boundaries

Defines the input and output ports for the workflow step. The boundaries parameter includes one input and up to two outputs.

The `input` and `outputs` can take the following port values:

- `false` - Used for branching.
- `true` - Used for branching.
- `untranslated` - Represents an untranslated string.
- `translated` - Represents a translated string.
- `approved` - Represents an approved string.
- `skipped` - Represents a string not sent to a vendor (applicable only for vendor-specific blocks).
- `all` - Represents any output.
- `initial` - Used for connecting to the start step.

### Editor Mode

Defines the editor mode for the workflow step. To allow view strings in the editor, this attribute should be defined. Read more about the [editor modes](https://support.crowdin.com/online-editor/#editor-modes).

Available values:

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

### Settings UI Module

Object with the settings UI module configuration. This optional parameter allows you to define a UI for workflow step settings.

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](/app-project-module/user-interface/) documentation.

### `onStepSettingsSave` Function

This optional function is called after saving the workflow for custom data management.

##### Parameters
- `organizationId` - Crowdin organization ID.
- `projectId` - Crowdin project ID.
- `stepId` - Workflow step ID.
- `workflowId` - Workflow ID.
- `settings` - formData object .
- `context` - [Context object](/app-project-module/reference/context/).
- `client` - Crowdin API client.

### `onDeleteStep` Function

This optional function is called after saving the workflow if a step has been deleted, allowing for custom data management.

##### Parameters

- `organizationId` - Crowdin organization ID.
- `projectId` - Crowdin project ID.
- `stepId` - Workflow step ID.
- `workflowId` - Workflow ID.
- `context` - [Context object](/app-project-module/reference/context/).
- `client` - Crowdin API client.

## Webhooks
If you want to receive notifications when workflow step statuses are recalculated for strings, you need to declare a `webhooks` module and listen to the `string.status_on_step.recalculation_triggered` event.

### Implementation Example

```js title="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](/app-project-module/tools/webhook/).

### Events Payload

#### Payload Structure

Each event in the `events` array contains the following structure:

| Field | Type | Description |
|-------|------|-------------|
| `event` | `string` | Event name: `string.status_on_step.recalculation_triggered` |
| `stringStatus.status` | `string` | Current step status. See [Status Values](#status-values) |
| `stringStatus.output` | `string` | Output from the workflow step when the status is `DONE` |
| `stringStatus.originEvent` | `string` | Action that triggered the recalculation. See [Origin Events](#origin-events) |
| `stringStatus.organizationId` | `string` | Crowdin organization ID |
| `stringStatus.translation` | `object` | Source string details (id, key, text, file, project, etc.) |
| `stringStatus.sourceLanguage` | `object` | Source language information |
| `stringStatus.affectedLanguage` | `object` | Target language affected by this step |
| `stringStatus.workflowStep` | `object` | Workflow step that triggered the event |
| `stringStatus.user` | `object` | User who triggered the action |

#### Event Example

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

```json collapse={10-24, 27-39, 43-61, 66-75, 79-88, 92-98, 102-104}
{
  "event": "string.status_on_step.recalculation_triggered",
  "stringStatus": {
    "status": "NEED_PROCESS",
    "output": "",
    "originEvent": "string.added",
    "organizationId": "200000001",
    "translation": {
      "id": 1000001,
      "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",
      "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
        },
        "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"
      }
    },
    "sourceLanguage": {
      "id": "en",
      "name": "English",
      "editorCode": "en",
      "twoLettersCode": "en",
      "threeLettersCode": "eng",
      "locale": "en-US",
      "androidCode": "en-rUS",
      "osxCode": "en.lproj",
      "osxLocale": "en",
      "textDirection": "ltr",
      "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",
      "dialectOf": null
    },
    "workflowStep": {
      "id": 1000,
      "title": "Translation Delay",
      "type": "Application",
      "languages": ["uk", "de", "fr"],
      "applicationModule": {
        "applicationIdentifier": "workflow-step-delay",
        "moduleKey": "workflow-step-delay-translation_delay"
      }
    },
    "user": {
      "id": "1",
      "username": "john_doe",
      "fullName": "John Doe",
      "avatarUrl": "https://example.crowdin.com/avatar/1/small/avatar_default.png"
    }
  }
}
```

#### Status Values

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

| Status | Description |
|--------|-------------|
| `NEED_PROCESS` | String needs to be handled by application |
| `TODO` | String is awaiting action |
| `DONE` | String was successfully handled |
| `FAILED` | Step failed to handle this string |
| `INCOMPLETE` | String was previously available in the workflow step but is now missing |

#### Origin Events

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