diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c2e9bb..4696f29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## Unreleased
+
+### New features
+
+- `udf` command to manage Cosmos DB for NoSQL user-defined functions on the current container: `list`, `show`, `exists` (returns a boolean usable in `if`/`while` conditions), `create` (from a JavaScript file or piped body, with `--force` to replace), and `delete`. ([#103](https://github.com/Azure/CosmosDBShell/issues/103))
+- `trigger` command to manage Cosmos DB for NoSQL triggers on the current container: `list`, `show`, `exists` (returns a boolean usable in `if`/`while` conditions), `create` (from a JavaScript file or piped body, with `--type` for pre/post, `--operation` for the operation, and `--force` to replace), and `delete`. ([#103](https://github.com/Azure/CosmosDBShell/issues/103))
+
## 1.1.4-preview — 2026-05-21
First release on the 1.1 line. A pretty packed cycle. The headline change is **ARM-based control plane for database and container management**, but there’s also a fully reworked CLI, two new item commands, a much friendlier shell experience for newcomers, and a long list of paper-cut fixes.
diff --git a/CosmosDBShell.Tests/CommandTests/TriggerCommandTests.cs b/CosmosDBShell.Tests/CommandTests/TriggerCommandTests.cs
new file mode 100644
index 0000000..ee6ee02
--- /dev/null
+++ b/CosmosDBShell.Tests/CommandTests/TriggerCommandTests.cs
@@ -0,0 +1,66 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace CosmosShell.Tests.CommandTests;
+
+using Azure.Data.Cosmos.Shell.Commands;
+using Azure.Data.Cosmos.Shell.Core;
+using Microsoft.Azure.Cosmos.Scripts;
+
+///
+/// Unit tests for the pure helpers on : subcommand
+/// normalization and trigger type/operation parsing.
+///
+public class TriggerCommandTests
+{
+ [Theory]
+ [InlineData("LIST", "list")]
+ [InlineData(" Show ", "show")]
+ [InlineData("Create", "create")]
+ [InlineData(null, "")]
+ [InlineData("", "")]
+ public void NormalizeSubcommand_TrimsAndLowercases(string? input, string expected)
+ {
+ Assert.Equal(expected, TriggerCommand.NormalizeSubcommand(input));
+ }
+
+ [Theory]
+ [InlineData("pre", TriggerType.Pre)]
+ [InlineData("PRE", TriggerType.Pre)]
+ [InlineData(" post ", TriggerType.Post)]
+ public void ParseTriggerType_ValidValues_AreParsed(string input, TriggerType expected)
+ {
+ Assert.Equal(expected, TriggerCommand.ParseTriggerType(input));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("middle")]
+ public void ParseTriggerType_InvalidValues_Throw(string? input)
+ {
+ Assert.Throws(() => TriggerCommand.ParseTriggerType(input));
+ }
+
+ [Theory]
+ [InlineData(null, TriggerOperation.All)]
+ [InlineData("", TriggerOperation.All)]
+ [InlineData("all", TriggerOperation.All)]
+ [InlineData("Create", TriggerOperation.Create)]
+ [InlineData("replace", TriggerOperation.Replace)]
+ [InlineData("DELETE", TriggerOperation.Delete)]
+ [InlineData(" update ", TriggerOperation.Update)]
+ public void ParseTriggerOperation_ValidValues_AreParsed(string? input, TriggerOperation expected)
+ {
+ Assert.Equal(expected, TriggerCommand.ParseTriggerOperation(input));
+ }
+
+ [Theory]
+ [InlineData("insert")]
+ [InlineData("bogus")]
+ public void ParseTriggerOperation_InvalidValues_Throw(string input)
+ {
+ Assert.Throws(() => TriggerCommand.ParseTriggerOperation(input));
+ }
+}
diff --git a/CosmosDBShell.Tests/CommandTests/UdfCommandTests.cs b/CosmosDBShell.Tests/CommandTests/UdfCommandTests.cs
new file mode 100644
index 0000000..0a854fb
--- /dev/null
+++ b/CosmosDBShell.Tests/CommandTests/UdfCommandTests.cs
@@ -0,0 +1,24 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace CosmosShell.Tests.CommandTests;
+
+using Azure.Data.Cosmos.Shell.Commands;
+
+///
+/// Unit tests for the pure helpers on .
+///
+public class UdfCommandTests
+{
+ [Theory]
+ [InlineData("LIST", "list")]
+ [InlineData(" Show ", "show")]
+ [InlineData("Create", "create")]
+ [InlineData(null, "")]
+ [InlineData("", "")]
+ public void NormalizeSubcommand_TrimsAndLowercases(string? input, string expected)
+ {
+ Assert.Equal(expected, UdfCommand.NormalizeSubcommand(input));
+ }
+}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/TriggerCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/TriggerCommand.cs
new file mode 100644
index 0000000..c3acd33
--- /dev/null
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/TriggerCommand.cs
@@ -0,0 +1,360 @@
+//------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//------------------------------------------------------------
+
+namespace Azure.Data.Cosmos.Shell.Commands;
+
+using System.Net;
+using System.Text.Json;
+using Azure.Data.Cosmos.Shell.Mcp;
+using Azure.Data.Cosmos.Shell.Parser;
+using Azure.Data.Cosmos.Shell.Util;
+using global::Azure.Data.Cosmos.Shell.Core;
+using global::Azure.Data.Cosmos.Shell.States;
+using Microsoft.Azure.Cosmos.Scripts;
+using Spectre.Console;
+
+[CosmosCommand("trigger")]
+[CosmosExample("trigger list", Description = "List the triggers in the current container")]
+[CosmosExample("trigger show myTrigger", Description = "Display the body of a trigger")]
+[CosmosExample("trigger create myTrigger ./myTrigger.js --type pre --operation create", Description = "Create a pre-trigger for create operations")]
+[CosmosExample("trigger create myTrigger ./myTrigger.js --type post --operation all --force", Description = "Create or replace a post-trigger for all operations")]
+[CosmosExample("trigger delete myTrigger", Description = "Delete a trigger")]
+#pragma warning disable SA1118 // Parameter should not span multiple lines
+[McpAnnotation(
+ Title = "Triggers",
+ Description = @"
+Manages JavaScript triggers on the current Cosmos DB container through subcommands:
+- 'list' returns the trigger ids in the container with their type and operation.
+- 'show ' returns the body of a trigger.
+- 'create ' creates a trigger from a JavaScript file. --type selects pre or post, --operation selects all/create/replace/delete/update, and --force replaces an existing one.
+- 'delete ' removes a trigger.
+This command is restricted in MCP. Run it manually in the shell.
+",
+ Restricted = true,
+ Destructive = true,
+ OpenWorld = true)]
+#pragma warning restore SA1118 // Parameter should not span multiple lines
+internal class TriggerCommand : CosmosCommand
+{
+ [CosmosParameter("subcommand", RequiredErrorKey = "command-trigger-error-missing_subcommand")]
+ public string Subcommand { get; init; } = string.Empty;
+
+ [CosmosParameter("name", IsRequired = false)]
+ public string? Name { get; init; }
+
+ [CosmosParameter("value", IsRequired = false)]
+ public string? Value { get; init; }
+
+ [CosmosOption("type", "t")]
+ public string? Type { get; init; }
+
+ [CosmosOption("operation", "op")]
+ public string? Operation { get; init; }
+
+ [CosmosOption("force", "f")]
+ public bool? Force { get; init; }
+
+ [CosmosOption("database", "db")]
+ public string? Database { get; init; }
+
+ [CosmosOption("container", "con")]
+ public string? Container { get; init; }
+
+ ///
+ /// Normalizes a subcommand token to its canonical lower-case form.
+ ///
+ internal static string NormalizeSubcommand(string? value) => (value ?? string.Empty).Trim().ToLowerInvariant();
+
+ ///
+ /// Parses the --type option into a . Accepts
+ /// 'pre' and 'post' (case-insensitive).
+ ///
+ internal static TriggerType ParseTriggerType(string? value)
+ {
+ return (value ?? string.Empty).Trim().ToLowerInvariant() switch
+ {
+ "pre" => TriggerType.Pre,
+ "post" => TriggerType.Post,
+ _ => throw new CommandException(
+ "trigger",
+ MessageService.GetArgsString("command-trigger-error-invalid_type", "type", value ?? string.Empty)),
+ };
+ }
+
+ ///
+ /// Parses the --operation option into a .
+ /// Defaults to when no value is supplied.
+ ///
+ internal static TriggerOperation ParseTriggerOperation(string? value)
+ {
+ return (value ?? string.Empty).Trim().ToLowerInvariant() switch
+ {
+ "" or "all" => TriggerOperation.All,
+ "create" => TriggerOperation.Create,
+ "replace" => TriggerOperation.Replace,
+ "delete" => TriggerOperation.Delete,
+ "update" => TriggerOperation.Update,
+ _ => throw new CommandException(
+ "trigger",
+ MessageService.GetArgsString("command-trigger-error-invalid_operation", "operation", value ?? string.Empty)),
+ };
+ }
+
+ public async override Task ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token)
+ {
+ var subcommand = NormalizeSubcommand(this.Subcommand);
+ if (subcommand.Length == 0)
+ {
+ throw new CommandException("trigger", MessageService.GetString("command-trigger-error-missing_subcommand"));
+ }
+
+ if (shell.State is not ConnectedState connectedState)
+ {
+ throw new NotConnectedException("trigger");
+ }
+
+ var (_, _, container) = await ResolveContainerAsync(
+ connectedState.Client,
+ shell.State,
+ this.Database,
+ this.Container,
+ "trigger",
+ token);
+
+ return subcommand switch
+ {
+ "list" or "ls" => await this.ListAsync(container, commandState, token),
+ "show" or "cat" => await this.ShowAsync(container, commandState, token),
+ "exists" => await this.ExistsAsync(container, commandState, token),
+ "create" or "set" => await this.CreateAsync(container, commandState, token),
+ "delete" or "rm" => await this.DeleteAsync(container, commandState, token),
+ _ => throw new CommandException(
+ "trigger",
+ MessageService.GetArgsString("command-trigger-error-invalid_subcommand", "subcommand", subcommand)),
+ };
+ }
+
+ private static CommandException NotFound(string name, Exception inner) =>
+ new("trigger", MessageService.GetArgsString("command-trigger-error-not_found", "name", name), inner);
+
+ private async Task ListAsync(Container container, CommandState commandState, CancellationToken token)
+ {
+ var items = new List