The Composer extension APIs
APPLIES TO: Composer v1.x and v2.x
It's possible to extend and customize the behavior of Composer at the application level by adding extensions. Extensions can hook into the internal mechanisms of Composer and change the way it operates. Extensions can also listen to activities inside Composer and react to them.
What's a Composer extension?
A Composer extension is a JavaScript module. When Composer starts, it loads extensions from its extensions
subfolder. When a module is loaded, it has access to a set of Composer APIs, which it can use to provide functionality to the application.
Extensions don't have access to the entire Composer application. They're granted limited access to specific areas of the application, and must adhere to a set of interfaces and protocols.
Composer extension APIs
Extensions currently have access to the following functional areas:
- Authentication and identity: extensions can provide a mechanism to gate access to the application and mechanisms used to provide user identity.
- Storage: extensions can override the built-in filesystem storage with a new way to read, write and access bot projects.
- Web server: extensions can add additional web routes to Composer's web server instance.
- User interface: extensions can add custom user interfaces to different parts of the application
- Publishing: extensions can add publishing mechanisms.
- Runtime templates: extensions can provide a runtime template used when ejecting from Composer.
Combining these endpoints, it's possible to achieve scenarios such as:
- Store content in a database.
- Require login via Microsoft Entra ID or any other oauth provider.
- Create a custom login screen.
- Require login via GitHub, and use GitHub credentials to store content in a Git repo automatically.
- Use Microsoft Entra ID roles to gate access to content.
- Publish content to external services such as remote runtimes, content repositories, and testing systems.
- Add custom authoring experiences inside Composer.
How to build an extension
Extension modules must come in one of the following forms:
- Default export is a function that accepts the Composer extension API.
- Default export is an object that includes an
initialize
function that accepts the Composer extension API. - A function called
initialize
is exported from the module.
Extensions can be loaded into Composer with the following method:
- The extension is placed in the
/extensions/
folder, and contains apackage.json
file with a configuredcomposer
definition.
The simplest form of an extension module is below:
export default async (composer: any): Promise<void> => {
// call methods (see below) on the composer API
// composer.useStorage(...);
// composer.usePassportStrategy(...);
// composer.addWebRoute(...)
}
Authentication and identity
To provide auth and identity services, Composer has in large part adopted PassportJS instead of implementing a custom solution. Extensions can use one of the many existing Passport strategies, or provide a custom strategy.
composer.usePassportStrategy(strategy)
Configure a Passport strategy to be used by Composer. This is the equivalent of calling app.use(passportStrategy)
on an Express app. See PassportJS docs.
In addition to configuring the strategy, extensions will also need to use composer.addWebRoute
to expose login, logout, and other related routes to the browser.
Calling this method also enables a basic auth middleware that is responsible for gating access to URLs and providing a simple user serializer/deserializer. Developers may choose to override these components using the methods below.
composer.useAuthMiddleware(middleware)
Provide a custom middleware for testing the authentication status of a user. This will override the built-in auth middleware that is enabled by default when calling usePassportStrategy()
.
Developers may choose to override this middleware for various reasons, such as:
- Apply different access rules based on URL
- Do something more than check
req.isAuthenticated
, such as validate or refresh tokens, make database calls, and provide telemetry.
composer.useUserSerializers(serialize, deserialize)
Provide custom serialize and deserialize functions for storing and retrieving the user profile and identity information in the Composer session.
By default, the entire user profile is serialized to JSON and stored in the session. If this isn't desirable, extensions should override these methods and provide alternate methods.
For example, the below code demonstrates storing only the user ID in the session during serialization, and the use of a database to load the full profile out of a database using that ID during deserialization.
const serializeUser = function(user, done) {
done(null, user.id);
};
const deserializeUser = function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
};
composer.useUserSerializers(serializeUser, deserializeUser);
composer.addAllowedUrl(url)
Allow access to a URL, without requiring authentication. The url
parameter can be an Express-style route with wildcards, such as /auth/:stuff
or /auth(.*)
.
This is primarily for use with authentication-related URLs. While the /login
route is allowed by default, any other URL involved in authentication needs to be added to the allowlist.
For example, when using OAuth, there's a secondary URL for receiving the auth callback. This has to be in the allowlist, otherwise access will be denied to the callback URL and it will fail.
// define a callback url
composer.addWebRoute('get','/oauth/callback', someFunction);
// Add the callback to the allow list.
composer.addAllowedUrl('/oauth/callback');
plugLoader.loginUri
This value is used by the built-in authentication middleware to redirect the user to the login page. By default, it's set to '/login' but it can be reset by changing this member value.
If you specify an alternate URI for the login page, you must use addAllowedUrl
to add it to the list of allowed callback URLs.
pluginLoader.getUserFromRequest(req)
This is a static method on the PluginLoader class that extracts the user identity information provided by Passport. It's used for web route implementations to get user identity information and provide it to other components of Composer.
For example:
const RequestHandlerX = async (req, res) => {
const user = await PluginLoader.getUserFromRequest(req);
// ... do some stuff
};
Storage
By default, Composer reads and writes assets to the local filesystem. Extensions may override this behavior by providing a custom implementation of the IFileStorage
interface. See interface definition here
Though this interface is modeled after a filesystem interaction, the implementation of these methods doesn't require using the filesystem, or a direct implementation of folder and path structure. However, the implementation must respect that structure and respond in the expected ways. For instance, the glob
method must treat path patterns the same way the filesystem glob would.
composer.useStorage(customStorageClass)
Provide an iFileStorage-compatible class to Composer.
The constructor of the class will receive two parameters: a StorageConnection
configuration, pulled from Composer's global configuration (currently data.json
), and a user identity object, as provided by any configured authentication extension.
The current behavior of Composer is to instantiate a new instance of the storage accessor class each time it's used. As a result, caution must be taken not to undertake expensive operations each time. For example, if a database connection is required, the connection might be implemented as a static member of the class, inside the extension's init code and made accessible within the extension module's scope.
The user identity provided by a configured authentication extension can be used for purposes such as:
- provide a personalized view of the content
- gate access to content based on identity
- create an audit log of changes
If an authentication extension isn't configured, or the user isn't logged in, the user identity will be undefined
.
The class is expected to be in the form:
class CustomStorage implements IFileStorage {
constructor(conn: StorageConnection, user?: UserIdentity) {
// ...
}
// ...
}
Web server
Extensions can add routes and middleware to the Express instance.
These routes are responsible for providing all necessary dependent assets, such as browser JavaScript and CSS.
Custom routes aren't rendered inside the front-end React application, and currently have no access to that application. They're independent pages—though nothing prevents them from making calls to the Composer server APIs.
composer.addWebRoute(method, url, callbackOrMiddleware, callback)
This is equivalent to using app.get()
or app.post()
. A simple route definition receives three parameters—the method, URL and handler callback function.
If a route-specific middleware is necessary, it should be specified as the third parameter, making the handler callback function the fourth.
The signature for callbacks is (req, res) => {}
.
The signature for middleware is (req, res, next) => {}
.
For example:
// simple route
composer.addWebRoute('get', '/hello', (req, res) => {
res.send('HELLO WORLD!');
});
// route with custom middleware
composer.addWebRoute('get', '/logout', (req, res, next) => {
console.warn('user is logging out!');
next();
},(req, res) => {
req.logout();
res.redirect('/login');
});
composer.addWebMiddleware(middleware)
Bind an additional custom middleware to the web server. Middleware applied this way will be applied to all routes.
The signature for middleware is (req, res, next) => {}
.
For middleware dealing with authentication, extensions must use useAuthMiddleware()
as otherwise the built-in auth middleware will still be in place.
User interface
Extensions can host and serve custom UI in the form of a bundled React application at select entries called contribution points.
A detailed set of explanation, documentation, and examples are on GitHub.
Publishing
composer.addPublishMethod(publishMechanism, schema, instructions)
By default, the publish method will use the name and description from the package.json file. However, you may provide a customized name:
composer.addPublishMethod(publishMechanism, schema, instructions, customDisplayName, customDisplayDescription);
This provides a new mechanism by which a bot project is transferred from Composer to some external service. The mechanisms can use whichever method necessary to process and transmit the bot project to the desired external service, though it must use a standard signature for the methods.
In most cases, the extension itself does NOT include the configuration information required to communicate with the external service. Configuration is provided by the Composer application at invocation time.
Once registered as an available method, users can configure specific target instances of that method on a per-bot basis. For example, a user may install a Publish to PVA extension, which implements the necessary protocols for publishing to PVA. Then, in order to actually perform a publish, they would configure an instance of this mechanism, "Publish to HR Bot Production Slot", that includes the necessary configuration information.
Publishing extensions support the following features:
- publish—given a bot project, publish it. Required.
- getStatus—get the status of the most recent publish. Optional.
- getHistory—get a list of historical publish actions. Optional.
- rollback—roll back to a previous publish (as provided by getHistory). Optional.
publish(config, project, metadata, user)
This method is responsible for publishing the project
using the provided config
using whatever method the extension is implementing—for example, publish to Azure. This method is required for all publishing extensions.
To publish a project, this method must perform any necessary actions such as:
- The LUIS lubuild process
- Calling the appropriate runtime
buildDeploy
method - Doing the actual deploy operation
Note
Language Understanding (LUIS) will be retired on 1 October 2025. Beginning 1 April 2023, you won't be able to create new LUIS resources. A newer version of language understanding is now available as part of Azure AI Language.
Conversational language understanding (CLU), a feature of Azure AI Language, is the updated version of LUIS. For more information about question-and-answer support in Composer, see Natural language processing.
Parameters:
Parameter | Description |
---|---|
config | An object containing information from both the publishing profile and the bot's settings—see below. |
project | An object representing the bot project. |
metadata | Any comment passed by the user during publishing. |
user | A user object if one has been provided by an authentication extension. |
Config will include:
{
templatePath: '/path/to/runtime/code',
fullSettings: {
// all of the bot's settings from project.settings, but also including sensitive keys managed in-app.
// this should be used instead of project.settings which may be incomplete
},
profileName: 'name of publishing profile',
... // All fields from the publishing profile
}
The project will include:
{
id: 'bot id',
dataDir: '/path/to/bot/project',
files: // A map of files including the name, path and content
settings: {
// content of settings/appsettings.json
}
}
Below is a simplified implementation of this process:
const publish = async(config, project, metadata, user) => {
const { fullSettings, profileName } = config;
// Prepare a copy of the project to build
// Run the lubuild process
// Run the runtime.buildDeploy process
// Now do the final actual deploy somehow...
}
getStatus(config, project, user)
This method is used to check for the status of the most recent publish of project
to a given publishing profile defined by the config
field. This method is required for all publishing extensions.
This endpoint uses a subset of HTTP status codes to report the status of the deploy:
Status | Meaning |
---|---|
200 | Publish completed successfully |
202 | Publish is underway |
404 | No publish found |
500 | Publish failed |
config
will be in the form below. config.profileName
can be used to identify the publishing profile being queried.
{
profileName: `name of the publishing profile`,
... // all fields from the publishing profile
}
Should return an object in the form:
{
status: [200|202|404|500],
result: {
message: 'Status message to be displayed in publishing UI',
log: 'any log output from the process so far',
comment: 'the user specified comment associated with the publish',
endpointURL: 'URL to running bot for use with Emulator as appropriate',
id: 'a unique identifier of this published version',
}
}
getHistory(config, project, user)
This method is used to request a history of publish actions from a given project
to a given publishing profile defined by the config
field. This is an optional feature—publishing extensions may exclude this functionality if it's not supported.
config
will be in the form below. config.profileName
can be used to identify the publishing profile being queried.
{
profileName: `name of the publishing profile`,
... // all fields from the publishing profile
}
Should return in array containing recent publish actions along with their status and log output.
[{
status: [200|202|404|500],
result: {
message: 'Status message to be displayed in publishing UI',
log: 'any log output from the process so far',
comment: 'the user specified comment associated with the publish',
id: 'a unique identifier of this published version',
}
}]
rollback(config, project, rollbackToVersion, user)
This method is used to request a rollback in the deployed environment to a previously published version. This DOES NOT affect the local version of the project. This is an optional feature—publishing extensions may exclude this functionality if it's not supported.
config
will be in the form below. config.profileName
can be used to identify the publishing profile being queried.
{
profileName: `name of the publishing profile`,
... // all fields from the publishing profile
}
rollbackToVersion
will contain a version ID as found in the results from getHistory
.
Rollback should respond using the same format as publish
or getStatus
and should result in a new publishing task:
{
status: [200|202|404|500],
result: {
message: 'Status message to be displayed in publishing UI',
log: 'any log output from the process so far',
comment: 'the user specified comment associated with the publish',
endpointURL: 'URL to running bot for use with Emulator as appropriate',
id: 'a unique identifier of this published version',
}
}
Runtime templates
composer.addRuntimeTemplate(templateInfo)
Expose a runtime template to the Composer UI. Registered templates will become available in the Runtime settings tab. When selected, the full content of the path
will be copied into the project's runtime
folder. Then, when a user selects Start Bot
, the startCommand
will be executed. The expected result is that a bot application launches and is made available to communicate with the Bot Framework Emulator.
await composer.addRuntimeTemplate({
key: 'myUniqueKey',
name: 'My Runtime',
path: __dirname + '/path/to/runtime/template/code',
startCommand: 'dotnet run',
build: async(runtimePath, project) => {
// implement necessary actions that must happen before project can be run
},
buildDeploy: async(runtimePath, project, settings, publishProfileName) => {
// implement necessary actions that must happen before project can be deployed to azure
return pathToBuildArtifacts;
},
});
build(runtimePath, project)
Perform any necessary steps required before the runtime can be executed from inside Composer when a user selects the Start Bot button. Note this method shouldn't actually start the runtime directly—only perform the build steps.
For example, this would be used to call dotnet build
in the runtime folder in order to build the application.
buildDeploy (runtimePath, project, settings, publishProfileName)
Parameter | Description |
---|---|
runtimePath | The path to the runtime that needs to be built. |
project | A bot project record. |
settings | A full set of settings to be used by the built runtime. |
publishProfileName | The name of the publishing profile that is the target of this build. |
Perform any necessary steps required to prepare the runtime code to be deployed. This method should return a path to the build artifacts with the expectation that the publisher can perform a deploy of those artifacts as is and have them run successfully. To do this, it should:
- Perform any necessary build steps
- Install dependencies
- Write
settings
to the appropriate location and format
composer.getRuntimeByProject(project)
Returns a reference to the appropriate runtime template based on the project's settings.
// load the appropriate runtime config
const runtime = composer.getRuntimeByProject(project);
// run the build step from the runtime, passing in the project as a parameter
await runtime.build(project.dataDir, project);
composer.getRuntime(type)
Get a runtime template by its key.
const dotnetRuntime = composer.getRuntime('csharp-azurewebapp');
Accessors
composer.passport
composer.name
Next steps
- Learn how to extend Composer with extensions.