llms.txt: machine-readable documentation index. Markdown version of this page is also available.

YouTrack Developer Portal Help

App Use Case: Send an Issue to an External Service

When part of your team's process takes place outside YouTrack, users might need to send selected issues to an external service without copying their details manually.

Building on this idea, create an app that adds an action to the issue toolbar. The action sends the current issue to an external service and records the identifier returned by the service. This app is most useful for solution developers and system administrators who build and maintain integrations for one or more projects.

The app includes:

Follow these steps to build and test the integration.

Prerequisites

To create an app like the one described in this tutorial, you need:

  • Basic knowledge of JavaScript.

  • Basic-level proficiency in using terminal commands.

  • Node.js and npm installed on your machine.

  • Access at the project admin level or higher to a YouTrack instance and a project where you can test the app.

  • A test endpoint for the external service and credentials that can access it.

  • Information about the request payload and response format expected by the external service.

Step 1 — Create an App Package

Prepare an empty directory for your app. Open the terminal, navigate to the app directory, and run the YouTrack app generator using the following command:

npm create @jetbrains/youtrack-app@latest

Create an app named external-service-integration with the title External Service Integration.

When the generator prompts you to create a widget, add a widget named Send to External Service with the key send-to-external-service. Select ISSUE_OPTIONS_MENU_ITEM as the extension point. This extension point adds an item to the issue toolbar that opens the widget.

For more information about creating an app package, see Create an App Package.

Step 2 — Update the Manifest

Open the manifest.json file and replace the widget declaration created by the generator with the following declaration:

{ "key": "send-to-external-service", "name": "Send to External Service", "indexPath": "send-to-external-service/index.html", "extensionPoint": "ISSUE_OPTIONS_MENU_ITEM", "description": "Sends the current issue to an external service.", "permissions": ["UPDATE_ISSUE"] }

The UPDATE_ISSUE permission limits the action to users who can update the current issue. The HTTP handler uses the same permission and updates the issue extension property only after the external service accepts the request.

For details about widget declarations and permission restrictions, see Widgets and App Permissions.

Step 3 — Configure the External Service

Create a settings.json file in the root directory of the app package. This file lets an administrator configure the service URL and credentials. Use project-level settings when each project connects to a different endpoint or account.

{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "External Service", "properties": { "serviceUrl": { "title": "Service URL", "type": "string", "x-scope": "PROJECT" }, "apiToken": { "title": "API Token", "type": "string", "format": "secret", "x-scope": "PROJECT" } }, "required": ["serviceUrl", "apiToken"] }

Enter the base URL of the external service in the Service URL setting. For example, if the service accepts requests at https://api.example.com/items, enter https://api.example.com. The HTTP handler adds the /items path.

The secret format keeps the token from being exposed to the widget or displayed after it is saved. The HTTP handler can use the value to authenticate requests sent with the YouTrack HTTP module.

For details, see Working with Settings for Secrets.

Step 4 — Add App Storage

Use an extension property to remember the identifier returned by the external service. This lets the app recognize issues that have already been sent and avoid creating duplicate external items.

Create an entity-extensions.json file in the root directory of the app package and declare a string property for issues:

{ "entityTypeExtensions": [ { "entityType": "Issue", "properties": { "externalId": { "type": "string", "description": "Identifier returned by the external service." } } } ] }

Use a regular custom field instead when users need to search, sort, or report on the value in the YouTrack UI.

Step 5 — Build the Widget Frontend

Open the src/widgets/send-to-external-service directory and replace the contents of index.html with the following markup:

<!doctype html> <html> <head> <meta charset="UTF-8"> <title>Send to External Service</title> </head> <body class="plugin"> <p>Send the current issue to the configured external service?</p> <button id="send-button" type="button">Send issue</button> <p id="status" role="status"></p> <script type="module" src="./index.js"></script> </body> </html>

Create an index.js file in the same directory and add the following code:

const host = await YTApp.register(); const sendButton = document.getElementById('send-button'); const status = document.getElementById('status'); sendButton.addEventListener('click', async () => { sendButton.disabled = true; status.textContent = 'Sending issue...'; try { const result = await host.fetchApp('backend/send', { method: 'POST', scope: true }); if (!result.success) { throw new Error(result.message || 'The external service rejected the request.'); } status.textContent = result.alreadySent ? 'This issue was already sent as ' + result.externalId + '.' : 'Issue sent as ' + result.externalId + '.'; } catch (error) { status.textContent = error.message || 'Unable to send the issue.'; sendButton.disabled = false; } });

The call to host.fetchApp() uses the current issue as its scope. The widget does not need to send an issue identifier or any service credentials to the handler.

The widget must not receive or send the service credentials. Keep authentication in the backend handler.

For details about registering a widget and calling an app endpoint, see Host API.

Step 6 — Add an HTTP Handler

The sample handler uses the following external API contract:

  • Send a POST request to <serviceUrl>/items.

  • Authenticate the request with the configured API token as a bearer token.

  • Send the YouTrack issue ID, summary, and description as a JSON object.

  • Read the external identifier from the id property in the JSON response.

For example, the external service should return a response in the following format:

{ "id": "EXT-123" }

Open the backend.js file in the src directory and replace its contents with the following handler:

const http = require('@jetbrains/youtrack-scripting-api/http'); exports.httpHandler = { endpoints: [ { scope: 'issue', method: 'POST', path: 'send', permissions: ['UPDATE_ISSUE'], handle: function handle(ctx) { const issue = ctx.issue; const savedExternalId = issue.extensionProperties.externalId; if (savedExternalId) { return ctx.response.json({ success: true, alreadySent: true, externalId: savedExternalId }); } const payload = { id: issue.idReadable, summary: issue.summary, description: issue.description || '' }; try { const connection = new http.Connection(ctx.settings.serviceUrl); connection.bearerAuth(ctx.settings.apiToken); connection.addHeader('Content-Type', 'application/json'); const serviceResponse = connection.postSync( '/items', null, JSON.stringify(payload) ); if (!serviceResponse.isSuccess) { return ctx.response.json({ success: false, message: 'External service returned status ' + serviceResponse.code + '.' }); } const responseData = serviceResponse.json(); if (!responseData || responseData.id == null) { return ctx.response.json({ success: false, message: 'External service did not return an identifier.' }); } const externalId = String(responseData.id); issue.extensionProperties.externalId = externalId; return ctx.response.json({ success: true, alreadySent: false, externalId: externalId }); } catch (error) { console.warn('Unable to send issue to external service: ' + error); return ctx.response.json({ success: false, message: 'Unable to contact the external service.' }); } } } ] };

This example sends a POST request to the /items path and expects a JSON response with an id property. Replace the path, payload, and response property with values supported by your external service. Replace bearerAuth() when the service uses a different authentication method.

The handler checks the extension property before sending the request, uses the current issue from the endpoint scope, and stores the external identifier only after the service returns a successful response.

For the HTTP handler format and an example that calls an external service, see Use Workflow API Modules in HTTP Handlers.

Step 7 — Build and Test the App

Build the app by running the following command in your terminal:

npm run build

Upload the app to a test YouTrack instance:

npm run upload -- --host <your YouTrack base URL> --token <permanent token>

Alternatively, upload the ZIP archive manually. For details, see Upload the App to YouTrack.

Attach the app to a test project. On the project-level Settings tab for the app, configure the service URL and API token, then activate the widget and HTTP handler.

Open an issue in the test project and verify the following behavior:

  • Sign in as a user with the Update Issue permission and confirm that the app action is available.

  • Select the app action and click Send issue.

  • Check that the external service receives the issue ID, summary, and description.

  • Check that the widget displays the identifier returned by the external service.

  • Select the action again and confirm that the app reports the existing identifier without creating a duplicate.

  • Use an invalid service URL and confirm that the app reports an error without storing an identifier or exposing the API token.

Summary

You've built an app that sends an issue to an external service from the issue toolbar. The app keeps the integration logic and credentials in a backend HTTP handler, while the widget handles confirmation and displays the result.

The app uses a secret setting for authentication and an issue extension property to remember the identifier returned by the external service.

Next Steps

Now that you've built the basic integration, you can extend it in several ways:

  • Add a link that opens the external item from the YouTrack issue.

  • Let users update an existing external item instead of creating another one.

  • Add project settings that map YouTrack fields to fields in the external service.

  • Add a workflow rule that sends issues automatically when they meet specific conditions.

  • Expose a separate webhook endpoint for updates initiated by the external service.

  • Add asynchronous processing for integrations that take too long to complete in the initial request.

02 October 2026