- Scaffolding the block and actions using the OpenOps CLI.
- Implementing custom API key-based authentication that enables the user to set up a connection.
- Developing a simple action that authenticates with YouTrack and retrieves data.
- Developing an advanced action that involves several dynamic, interdependent properties and updates a YouTrack issue.
Scaffolding a new block
To scaffold a new block, you run the following command:Block name:youtrack(lowercase).Package name: leave blank to use the default.Authentication type: selectCustom (Custom properties).Create opinionated folder structure with stubs for actions, tests, and common service layer: selectYes - Create full folder structure with stubs.
packages/blocks/youtrack/, including package.json, TypeScript configuration files, ESLint and Jest configuration files, an entry point for the block at src/index.ts, and a starter test file at test/index.test.ts.
Let’s take a closer look at the generated files.
src/index.ts
src/index.ts is the entry point to the new block. It uses the createBlock() function from the OpenOps block framework to create a new block. This function takes a number of arguments, of which the most important ones are:
auth: authentication implementation for the block. It defines which credentials the user needs to specify in the block’s connection properties to authenticate with the API that the block integrates with.actions: an array that contains the actions that the block exposes. The scaffolded version includes a predefined action that allows making a custom API call to the block’s API.
src/lib/auth.ts
Scaffolding generates a placeholder authentication implementation in a separate file. It uses theCustomAuth() helper function from the OpenOps block framework. The function takes several parameters, of which the most important ones are:
props: an object that defines the inputs required to authenticate with the service that the block integrates with.validate: a function for optional validation of the inputs defined inprops.
Project files
OpenOps uses Nx as its build system. Theproject.json file defines how the new block is built, tested, and linted within the Nx workspace. It specifies the source root, output path, TypeScript configuration, package entry points, and assets. It also declares executors for build (@nx/js:tsc), test (@nx/jest:jest), and lint (@nx/eslint:lint).
package.json is a minimal metadata file that declares the block’s name and version within the Nx workspace. Unlike typical npm packages, this file isn’t meant for publication to the npm registry. Instead, it helps Nx identify the project and marks the block as a versioned unit within the larger repository.
dependencies section of package.json.
Test files
test/index.test.ts is a stub Jest test fixture that includes a few sample tests for the block:
nx test blocks-youtrack.
There’s also a Jest configuration file, jest.config.ts, which inherits shared settings from the OpenOps Jest preset, references a TypeScript config file, defines recognized file extensions, and specifies where to store code coverage reports:
TypeScript configuration files
Scaffolding generates three TypeScript configuration files:tsconfig.json, tsconfig.lib.json, and tsconfig.spec.json.
tsconfig.json is the base config for the block. It extends the workspace’s root tsconfig.base.json and enforces strict compiler options but doesn’t include any source files directly. Instead, it references the other two config files, serving as an entry point for both:
tsconfig.lib.json applies to the block’s source code. It outputs compiled files to dist/out-tsc, generates type declarations, and includes only non-test TypeScript files under src/:
tsconfig.spec.json is tailored for testing. It includes test and declaration files and configures Jest typings:
Other files
.eslintrc.json defines JavaScript and TypeScript linting behavior for the block, extending the OpenOps repository’s root ESLint configuration. The overrides section provides placeholders for block-specific linting rules if you want to introduce them.
README.md stubs out documentation for the block.
Post-scaffolding cleanup
OpenOps may alter the spelling of your block’s display name during scaffolding, so review this and run search and replace if needed. For example, with YouTrack it didn’t use the proper casing for the product name, generating “Youtrack” instead of “YouTrack”.Product logo
OpenOps also generates a stub logo URL at thestatic.openops.com domain, which doesn’t exist. To see your product’s logo in the OpenOps UI during development, replace the stub logo URL with a URL you control. For example, for YouTrack, the stub URL https://static.openops.com/blocks/youtrack.png can be replaced with a live YouTrack logo URL.
When you open a PR to the OpenOps repository, the OpenOps team will upload your product’s logo to the OpenOps static server. Once this is done, you’ll need to replace your logo URL in the PR with the OpenOps-hosted one.
Categories
When scaffolding a block, OpenOps sets the block’scategories property to [BlockCategory.FINOPS]. The BlockCategory enum defines the categories that a block can belong to, such as:
FINOPS: integrations with tools that recommend cloud usage optimization opportunities.CLOUD: integrations with cloud providers.WORKFLOW: blocks providing workflow composition logic.COLLABORATION: integrations with project management and collaboration tools.DATA_SOURCES: integrations with databases, data warehouses, and other data sources.DEVOPS: integrations with version control and infrastructure-as-code (IaC) tools.
COLLABORATION.
Even basic scaffolding introduces the new block to the OpenOps UI, though the block doesn’t do much yet. Here’s what you can see at this point if you run npm run start and open the OpenOps frontend at http://localhost:4200/ in your browser:

Implementing authentication
Now that you have a block, you could start building its actions. However, unless your block interacts with a publicly accessible API, you need to provide the user with a way to authenticate with the API that your block is built on. How you implement authentication defines what the user sees when they try to create a new connection for your block. Scaffolding generated a placeholder authentication implementation and referenced it from the YouTrack block’s entry point. What’s left is to implement the actual authentication logic in thesrc/lib/auth.ts file. As a reminder, here’s what the scaffolded file initially looked like:
CustomAuth() helper function to define YouTrack authentication. There are other types of authentication, but CustomAuth() works best for YouTrack because, in addition to an API key, the user needs to specify a base URL. YouTrack can be cloud-hosted by JetBrains, with a unique base URL for each cloud instance, or self-hosted by customers.
The values of the authProviderKey, authProviderDisplayName, authProviderLogoUrl, and required properties are fine as they are. What needs to be changed are the values of props, validate, and description. Let’s start with props.
Defining connection properties
props defines the inputs required to authenticate with the service provider that the block integrates with. Each of these inputs is an OpenOps UI component: it can be a regular or masked input field, a checkbox, a dropdown, and more. This component library is used not only for defining connections but also for defining the actions that a block exposes.
The first property, apiKey, is for the user to provide a YouTrack API key (a.k.a. permanent token). The scaffolded version of this property uses the SecretText property type, which is a masked input field, and it’s exactly what we need for this kind of data. Let’s expand the scaffolded version of the property to include a description:
apiUrl, is for the user to provide the base URL of their YouTrack instance. This property is a regular input field. The scaffolded version is a good start, but we can improve it by adding a clearer description, providing a default value, and using a predefined validation rule for URLs:
@openops/blocks-framework package to include the Validators object:
Validators.url is not the only predefined validation rule. OpenOps provides a set of ready-to-use validation rules for value ranges, dates, emails, phone numbers, image formats, and more.
Adding custom validation
Adding predefined validation rules to properties is one way to enforce a specific format or pattern for user input. You can also add custom validation rules using thevalidate() function.
Some blocks, such as Linear, use validate() to ensure that the API key conforms to a specific format. With YouTrack, there’s no need for this. Instead, it makes sense to check that the base URL doesn’t have a trailing slash, since YouTrack doesn’t normalize URLs with double slashes. Here’s how this could be done:
auth object passed to validate() contains the values of all the properties provided by the user. This lets you introduce additional checks on auth.apiUrl, which holds the entered base URL.
You could also use validate() to try authenticating the user with a given API key. For example, the ServiceNow and Umbrella blocks make sample requests to their APIs to let the user know immediately if authentication fails.
Adding a Markdown description
When a user sets up a connection, they may not immediately know how to obtain an API key or another value required during connection setup. To guide them, use thedescription property. The best part about this property is that it isn’t limited to plain text — you can provide a Markdown-formatted string, and OpenOps will render it in the connection UI, between the title and the first input.
For YouTrack, a Markdown description could look like this:
description property:
Adding an interface
The last step in thesrc/lib/auth.ts file is to define an interface for the two authentication properties. This interface will be imported into the files that define the block’s actions to provide typing for the action parameters.
For YouTrack authentication, the interface could look like this:
The resulting authentication code and connection UI
After implementing authentication, thesrc/lib/auth.ts file looks like this:



Adding a simple action
We have a block, we’ve implemented authentication, and now it’s time to move on to the core of every integration: actions.Removing the default action
When you scaffold a block, OpenOps adds an action called Custom API Call. While we could tweak it to work for YouTrack, let’s instead remove it and create a new action from scratch. After removing the scaffolded action, thesrc/index.ts file looks like this:
actions array is now empty. Once we create an action, we’ll need to add a reference to it there.
Scaffolding the new action
OpenOps provides scaffolding for actions as well, so let’s run this in the terminal:Enter the block folder name: the folder where the block for the new action resides. For the existing YouTrack block, the value should beyoutrack.Enter the action display name: a human-readable name for the action that users will see in the UI. It’s also used to name the action’s file. In this case, we want an action that lists issues in a YouTrack instance, so let’s call itGet all issues.Enter the action description: a brief, informative text shown in the UI to explain the action’s function and purpose. Let’s enterRetrieves all issues from a YouTrack instance.Does this action modify data or state (e.g., create, update, delete)?: enternfor “No” as this is a read-only action.
get-all-issues.ts in the YouTrack block’s /src/lib/actions directory. The file initially looks like this:
createAction() helper function to define the action. The values of displayName and description are the same as the values provided in the CLI.
The props property is empty, which means there are no configuration options for the user to set. For this first action, that’s fine; we’ll add another action with configuration options later.
Adding action execution logic
The scaffolded action has two prominent omissions:- It doesn’t have an
authproperty referencing the block’s authentication logic implemented earlier. - Its
run()function, which should define the action logic, is empty.
- The action now has an
authproperty that references the block’s authentication logic. Both the authentication logic and its interface are imported from the block’sauth.tsfile. - The
run()function now makes a request to a YouTrack API endpoint to retrieve the first 10 unresolved issues. - To make the API call, the
run()function uses the importedhttpClientconstant, which is an OpenOps-provided wrapper around the Axios HTTP client. Some blocks, such as Linear and Jira, create separate functions for making HTTP requests to their APIs. This can be useful for providing extra error handling or leveraging existing SDKs, but for YouTrack, we can just usehttpClientdirectly. - The
returnstatement of therun()function defines what the user sees as the output of the action. The format of the returned object is up to the block author. In this case, we’re returning an object with two properties:_debugandissues. The_debugproperty is a container for debugging information useful during development. Theissuesproperty is an array of issues retrieved from the YouTrack instance.
Referencing the action from the block definition
Before testing the action, we need to add it to the block’s definition. In thesrc/index.ts file, we need to import the action and add it to the actions array:
Testing the action
Let’s see what happens in the OpenOps UI now that the action is in place. First, the action is visible in the workflow editor and available for selection:

return statement in the action’s run() function:

Adding an action with properties
We’ve seen how to create a simple action that doesn’t require any configuration. More often than not, real-world actions do require the user to enter or select values in the properties pane, and the values of these properties often depend on one another. For a YouTrack integration, it makes sense to enable the user not only to get the list of issues but also to update specific issues. Let’s add a new action that allows the user to change the status of an issue. This action will include several configurable properties, and one of these properties will depend on the value of another.Scaffolding the new action
First, let’s use the OpenOps scaffolding to create a stub for the new action:Enter the block folder name: as before, this should beyoutrack.Enter the action display name: let’s call the new actionChange issue status.Enter the action description: let’s describe the action asUpdates the status of a given issue.Does this action modify data or state (e.g., create, update, delete)?: it’sYfor “Yes” as this action does update the state of an external system.
change-issue-status.ts in the YouTrack block’s /src/lib/actions directory:
Referencing the action from the block definition
This time, let’s reference the new action in the block’s definition (src/index.ts) before working on the action logic:
Adding authentication and imports
Back in the action file, let’s start by referencing the authentication logic we defined earlier. While we’re at it, let’s also import theYouTrackAuth interface, along with the HTTP client that OpenOps provides:
Adding an interface representing a YouTrack project
Before adding properties, let’s define an interface for the YouTrack project that the issue being updated belongs to. We’ll use it both in the property that helps select a project and in another property that depends on it. We could create the interface in a separate file, but placing it at the end of thechange-issue-status.ts file works just as well:
Adding a dropdown property to select a project
We can now add the first property to theprops parameter of createAction(). This property represents a dropdown that lets the user select a YouTrack project that the issue being updated belongs to:
project property:
displayNameis how the dropdown for this property is labeled in the UI.requiredmarks this property as required. The action will not run unless the user selects a value.refreshersdefines when the values of this property should be re-evaluated. The valueauthin this array means that this property refreshes only when the connection used by this action changes.options()defines the logic for populating the values of this property. A quick check on the value of theauthproperty allows returning an empty set of values when no connection is defined.- If a connection is defined, it’s safe to make a request to the YouTrack API to retrieve the list of projects in the connected YouTrack instance.
- When the YouTrack API request succeeds, its response is transformed into an array of objects representing the projects. Each object has two properties:
labelandvalue. Thelabelproperty is the text shown in dropdown items, representing the name of each YouTrack project. Thevalueproperty contains additional information about the project retrieved from the API, including its ID and short name — these will be useful later when passed to another property. - The array is assigned to the
optionsproperty of a new object that serves as the return value of theoptions()function.


Adding an input property for an issue ID
The next property we’ll add toprops is an input field that lets the user enter the numeric part of the ID of the issue to update. This type of property is simple, declarative, and doesn’t contain any custom logic:

Adding a dropdown property for the new status
The final property we’ll add toprops is another dropdown that lets the user select the new status for the issue. This property shows different values depending on what’s selected in the Project dropdown.
- The
refreshersarray containsproject, meaning this dropdown refreshes every time a new value is selected in the Project dropdown. This is exactly what we want because different projects have different status values. - The
options()function receives two parameters:auth, which represents the block authentication, andproject, which holds the object representing the selected project. Remember thevalueproperty we returned from theoptions()of theprojectproperty? That’s what theprojectparameter contains here. - If either parameter is undefined — meaning a connection isn’t configured or a project isn’t selected — the dropdown is disabled.
- Two calls are made to YouTrack API endpoints to retrieve the list of status values in the selected project. The first call uses the
projectparameter, which reflects the selected value of the Project dropdown. If the project doesn’t have a Status field, the dropdown is disabled and a placeholder is shown. - Once the list of status values is obtained, it’s returned as an array in the
optionsproperty representing the dropdown items. Thelabelis the name of each status shown in the dropdown, andvaluecontains additional data that will be used later when the action is executed.


Adding action execution logic
The last step is to add the logic executed when the action runs. This logic makes a request to the YouTrack API to update the issue’s status, using the inputs provided by the user in the properties we added above.- The
run()function receives thecontextobject, which contains extensive data about the current action and the workflow’s execution state, including the block authentication (context.auth) and the values of all the action’s properties (context.propsValue). - The first call to the YouTrack API checks that the specified issue exists. In that call, we’re using the values from both the Project dropdown and the Number from issue ID input field.
- If the issue exists, we look for the part of its data that represents the status field. Once found, another YouTrack API call updates the status to the value specified in the New Status dropdown.
- The updated issue data is returned from the
run()function, and this is what the user sees as the output of the executed action.

change-issue-status.ts file:
Summary
This guide walked you through creating a fully functional third-party integration for OpenOps by building a JetBrains YouTrack block with two actions. Along the way, you learned techniques involved in developing OpenOps blocks, including:- Implementing custom authentication that requires specifying an API key and a base URL. To learn more about authentication types that OpenOps supports, see Authentication.
- Adding configuration properties to actions, including dynamic, interdependent properties.
- Using values returned by properties in other properties, as well as in the action’s execution logic.
