Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace TALXIS.CLI.Core.Platforms.PowerPlatform;

/// <summary>
/// A connector available in a Power Platform environment (e.g. shared_teams).
/// </summary>
public sealed record ConnectorSummary(
string Name,
string DisplayName,
string? Tier,
bool IsCustomApi,
string? Description);

/// <summary>
/// One operation (action or trigger) exposed by a connector, distilled from
/// its OpenAPI definition.
/// </summary>
public sealed record ConnectorOperationSummary(
string OperationId,
string Kind,
string? Summary,
string? Visibility,
string? Description);

/// <summary>
/// A single operation parameter. Body schema leaves are flattened into
/// slash-joined names (e.g. <c>item/subject</c>) — exactly the key format a
/// cloud flow definition uses in <c>inputs.parameters</c>.
/// </summary>
public sealed record ConnectorOperationParameter(
string Name,
string In,
string? Type,
bool Required,
string? Summary,
string? Description,
IReadOnlyList<string>? EnumValues,
bool IsDynamic);

/// <summary>
/// Full parameter-level detail of one connector operation, ready to be copied
/// into a cloud flow action (<c>host.apiId</c>, <c>host.operationId</c>,
/// <c>inputs.parameters</c> keys).
/// </summary>
public sealed record ConnectorOperationDetail(
string ConnectorName,
string ApiId,
string OperationId,
string Kind,
string? Summary,
string? Description,
string HttpMethod,
string Path,
IReadOnlyList<ConnectorOperationParameter> Parameters);

/// <summary>
/// Read-only discovery over the connectors available in the target
/// environment and their operations, backed by the Power Apps connector API.
/// Exists so agents authoring cloud flow definitions can look up exact
/// operation ids and parameter names instead of guessing them.
/// </summary>
public interface IConnectorCatalogService
{
/// <summary>Lists connectors available in the profile's environment.</summary>
Task<IReadOnlyList<ConnectorSummary>> ListConnectorsAsync(
string? profileName,
CancellationToken ct);

/// <summary>Lists all operations (actions and triggers) of one connector.</summary>
Task<IReadOnlyList<ConnectorOperationSummary>> ListOperationsAsync(
string? profileName,
string connectorName,
CancellationToken ct);

/// <summary>
/// Returns parameter-level detail for one operation, or null when the
/// operation does not exist on the connector.
/// </summary>
Task<ConnectorOperationDetail?> GetOperationAsync(
string? profileName,
string connectorName,
string operationId,
CancellationToken ct);
}
5 changes: 3 additions & 2 deletions src/TALXIS.CLI.Features.Docs/Skills/component-creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ Scaffold components in dependency order:
7. **Project Reference** — `dotnet add reference` from solution project to the separate project. The Build SDK auto-detects the project type and handles registration (assembly data.xml, web resource data.xml, etc.) during `dotnet build`
8. **Ribbon Buttons** (`pp-ribbon-button`) — after entity and script library exist. References the web resource via `LibraryLogicalName=prefix_name`
9. **Form Event Handlers** (`pp-form-event-handler`) — after entity, form, and script library exist. References via `libraryName=prefix_name.js` and `functionName=prefix_name.ClassName.methodName`
10. **Cloud Flows** (`pp-flow`) - after any entity the flow triggers on exists. Dataverse-trigger flows reference an existing connection reference via `ConnectionReferenceLogicalName` (see [flow-development](flow-development.md))

## Key Parameter Conventions

- **`LogicalName`** (in pp-entity, pp-entity-attribute, pp-optionset-global, pp-app-model): The name **without** publisher prefix. The template adds the prefix automatically. Example: `warehouseitem`, not `udpp_warehouseitem`.
- **`LogicalName`** (in pp-entity, pp-entity-attribute, pp-optionset-global, pp-app-model, pp-flow): The name **without** publisher prefix. The template adds the prefix automatically. Example: `warehouseitem`, not `udpp_warehouseitem`.
- **`EntitySchemaName`** (in pp-entity-attribute, pp-entity-form, pp-entity-view, pp-form-*): The entity name **with** publisher prefix. Example: `udpp_warehouseitem`.
- **`EntityLogicalName`** (in pp-sitemap-subarea, pp-app-model-component, pp-ribbon-*): The entity name **with** publisher prefix. Example: `udpp_warehouseitem`.
- **`EntityLogicalName`** (in pp-sitemap-subarea, pp-app-model-component, pp-ribbon-*, pp-flow): The entity name **with** publisher prefix. Example: `udpp_warehouseitem`.
- **`AppName`** (in pp-sitemap-*, pp-app-model-component): The app module folder name **with** publisher prefix. Example: `udpp_warehouseapp`.
- **`ReferencedEntityName`** (in pp-entity-attribute for Lookup types): The target entity **with** publisher prefix. Example: `udpp_warehouseitem`.
- **`Behavior`** (in pp-entity): Use `New` when creating the entity definition for the first time. Use `Existing` to add a reference to an entity owned by another solution (e.g., adding forms/views in a UI solution for an entity defined in the DataModel solution).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Custom APIs are custom messages (actions) in Dataverse that expose reusable busi

- **Custom API** — reusable callable endpoint, typed request/response, synchronous with return value
- **Plugin** — reactive logic triggered by data events (Create, Update, Delete)
- **Power Automate** — low-code automation with connectors and approval flows
- **Power Automate** — low-code automation with connectors and approval flows (see [flow-development](flow-development.md))

Choose Custom API when multiple clients or flows need to invoke the same operation.

Expand Down
48 changes: 48 additions & 0 deletions src/TALXIS.CLI.Features.Docs/Skills/flow-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Cloud Flow Development

## Key Concept

Power Automate cloud flows are solution components stored locally as source: a JSON definition (Logic Apps workflow schema) under `Workflows/` plus a sibling `.data.xml` metadata file (`Category=5`, `Type=1`). Scaffolding creates the flow locally - it reaches the environment through the normal solution deployment, never by clicking it together in the designer first.

## Flow Scaffolding Chain

1. **Scaffold the flow** → `workspace_component_create` with `componentType: "pp-flow"` and a `Trigger` choice:
- `manual` - instant flow started by a button (default)
- `recurrence` - scheduled flow; set `RecurrenceFrequency` (Minute/Hour/Day/Week/Month) and `RecurrenceInterval`
- `dataverse` - automated flow on a Dataverse row event; set `EntityLogicalName`, `TriggerEvent`, `Scope`, optional `FilteringAttributes`, and `ConnectionReferenceLogicalName`
2. **Author actions** by editing the generated `Workflows/<prefix>_<name>-<guid>.json` (rules below)
3. **Build locally** to validate and pack: `dotnet build`
4. Follow the [deployment workflow](deployment-workflow.md)

Call `workspace_component_parameter_list` for required parameters at each step.

## Dataverse Trigger Parameters

- **`TriggerEvent`** → `subscriptionRequest/message`: create=1, delete=2, update=3, create-or-update=4, create-or-delete=5, update-or-delete=6, create-or-update-or-delete=7
- **`Scope`** → `subscriptionRequest/scope`: user=1, business-unit=2, parent-child-business-unit=3, organization=4 (default)
- **`FilteringAttributes`** - comma-separated attribute logical names; the flow only fires when one of them changes (meaningful for events that include update)
- **`EntityLogicalName`** - entity logical name **with** publisher prefix (e.g. `udpp_warehouseitem`)
- **`ConnectionReferenceLogicalName`** - logical name of an **existing** Dataverse connection reference; connection references are declared under `<connectionreferences>` in `Other/Customizations.xml`

## Authoring Actions in the Definition JSON

- The definition must declare both `$connections` and `$authentication` parameters (the template already does)
- Standard connector actions use `"type": "OpenApiConnection"` (never `ApiConnection`); webhook-style operations (e.g. Approvals) use `"type": "OpenApiConnectionWebhook"`
- Every connector action's `host.connectionName` must match a key in `properties.connectionReferences`; each entry needs `"runtimeSource": "embedded"` and a real `connectionReferenceLogicalName`
- `runAfter` must reference existing action names; the first action uses `"runAfter": {}`
- Every `OpenApiConnection`/`OpenApiConnectionWebhook` trigger and action passes `"authentication": "@parameters('$authentication')"` inside `inputs` (real exported flows carry it for all connectors, not just Dataverse)
- Never guess connector `operationId`, `apiId`, or parameter names - discover them live: `environment_connector_list` (available connectors), `environment_connector_operation_list` (operations with kind action/trigger/webhook-trigger), `environment_connector_operation_get` (exact parameter names, types, enums; body leaves are already slash-joined like `emailMessage/To`)
- `webhook-trigger` operations go into `triggers` as `OpenApiConnectionWebhook`; parameters marked dynamic require values resolved at authoring time
- Dataverse `CreateRecord`/`UpdateRecord` bodies are dynamic: keys are `item/{attribute logical name}` from the entity schema, with the `@odata.bind` suffix for lookups (e.g. `item/ownerid@odata.bind`)
- Expressions use `@{...}` interpolation with functions like `triggerOutputs()`, `outputs('<Action>')?['body/field']`, `concat()`, `utcNow()`

## What NOT to Do

- ❌ Don't create flows directly in the environment - scaffold locally so the flow lives in source control
- ❌ Don't scaffold a `dataverse`-trigger flow before the triggering entity exists in the workspace
- ❌ Don't reference a connection reference that doesn't exist - declare it in `Other/Customizations.xml` first
- ❌ Don't invent connector operation parameters - mirror a real flow or a designer export
- ❌ Don't use premium triggers (e.g. HTTP request) without checking licensing; if a DLP policy blocks a trigger connector, fall back to a `recurrence` trigger that polls instead
- ❌ Don't rename the flow JSON file by hand - `JsonFileName` inside the `.data.xml` must keep matching it exactly

See also: [component-creation](component-creation.md), [deployment-workflow](deployment-workflow.md)
3 changes: 2 additions & 1 deletion src/TALXIS.CLI.Features.Docs/Skills/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
{"id": "pcf-controls", "title": "PCF Control Development", "summary": "PCF project structure, ControlManifest, lifecycle methods, dataset vs field controls, and build workflow.", "tags": ["workspace", "local-development", "pcf"]},
{"id": "build-errors", "title": "Build Error Recovery", "summary": "Diagnose and fix TALXISXSD001, TALXISGUID001, and other build validation errors.", "tags": ["troubleshooting", "build", "validation"]},
{"id": "data-querying", "title": "Data Querying", "summary": "Query Dataverse data using SQL, OData, and FetchXML — choosing the right language, OData patterns, aggregation, and pagination.", "tags": ["data-operations", "environment"]},
{"id": "security-roles", "title": "Security Role Scaffolding", "summary": "Scaffold security roles, add privileges, and assign roles to model-driven apps.", "tags": ["workspace", "local-development", "security"]}
{"id": "security-roles", "title": "Security Role Scaffolding", "summary": "Scaffold security roles, add privileges, and assign roles to model-driven apps.", "tags": ["workspace", "local-development", "security"]},
{"id": "flow-development", "title": "Cloud Flow Development", "summary": "Power Automate cloud flow structure, trigger selection, connection references, action authoring rules, and scaffolding with pp-flow.", "tags": ["workspace", "local-development", "flows"]}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DotMake.CommandLine;

namespace TALXIS.CLI.Features.Environment.Connector;

[CliCommand(
Name = "connector",
Description = "Discover connectors and their operations in the live environment (for authoring cloud flows).",
Children = new[]
{
typeof(ConnectorListCliCommand),
typeof(Operation.ConnectorOperationCliCommand),
},
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class ConnectorCliCommand
{
public void Run(CliContext context)
{
context.ShowHelp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using DotMake.CommandLine;
using Microsoft.Extensions.Logging;
using TALXIS.CLI.Core;
using TALXIS.CLI.Core.DependencyInjection;
using TALXIS.CLI.Core.Platforms.PowerPlatform;
using TALXIS.CLI.Logging;

namespace TALXIS.CLI.Features.Environment.Connector;

[CliReadOnly]
[CliCommand(
Name = "list",
Description = "List the connectors available in the profile's environment."
)]
public class ConnectorListCliCommand : ProfiledCliCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(ConnectorListCliCommand));

[CliOption(Name = "--filter", Description = "Show only connectors whose name or display name contains this substring.", Required = false)]
public string? Filter { get; set; }

protected override async Task<int> ExecuteAsync()
{
var service = TxcServices.Get<IConnectorCatalogService>();
IReadOnlyList<ConnectorSummary> connectors = await service.ListConnectorsAsync(Profile, CancellationToken.None)
.ConfigureAwait(false);

if (!string.IsNullOrWhiteSpace(Filter))
{
connectors = connectors
.Where(c => Contains(c.Name, Filter) || Contains(c.DisplayName, Filter))
.ToList();
}

OutputFormatter.WriteList(connectors, PrintTable);
return ExitSuccess;
}

private static bool Contains(string? value, string substring)
=> value is not null && value.Contains(substring, StringComparison.OrdinalIgnoreCase);

// Text-renderer callback invoked by OutputFormatter.WriteList — OutputWriter usage is intentional.
#pragma warning disable TXC003
private static void PrintTable(IReadOnlyList<ConnectorSummary> connectors)
{
if (connectors.Count == 0)
{
OutputWriter.WriteLine("No connectors found.");
return;
}

int nameWidth = Math.Clamp(connectors.Max(c => c.Name.Length), 20, 60);
int displayWidth = Math.Clamp(connectors.Max(c => c.DisplayName.Length), 12, 40);
string header = $"{"Name".PadRight(nameWidth)} | {"Display Name".PadRight(displayWidth)} | {"Tier".PadRight(10)} | Custom";
OutputWriter.WriteLine(header);
OutputWriter.WriteLine(new string('-', header.Length));

foreach (var c in connectors)
{
string name = c.Name.Length > nameWidth ? c.Name[..(nameWidth - 1)] + "." : c.Name;
string display = c.DisplayName.Length > displayWidth ? c.DisplayName[..(displayWidth - 1)] + "." : c.DisplayName;
OutputWriter.WriteLine(
$"{name.PadRight(nameWidth)} | {display.PadRight(displayWidth)} | {(c.Tier ?? string.Empty).PadRight(10)} | {(c.IsCustomApi ? "yes" : string.Empty)}");
}
}
#pragma warning restore TXC003
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DotMake.CommandLine;

namespace TALXIS.CLI.Features.Environment.Connector.Operation;

[CliCommand(
Name = "operation",
Description = "Inspect the operations (actions and triggers) a connector exposes.",
Children = new[]
{
typeof(ConnectorOperationListCliCommand),
typeof(ConnectorOperationGetCliCommand),
},
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class ConnectorOperationCliCommand
{
public void Run(CliContext context)
{
context.ShowHelp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using DotMake.CommandLine;
using Microsoft.Extensions.Logging;
using TALXIS.CLI.Core;
using TALXIS.CLI.Core.DependencyInjection;
using TALXIS.CLI.Core.Platforms.PowerPlatform;
using TALXIS.CLI.Logging;

namespace TALXIS.CLI.Features.Environment.Connector.Operation;

[CliReadOnly]
[CliCommand(
Name = "get",
Description = "Show one connector operation with its exact parameter names, types, and enum values for flow authoring."
)]
public class ConnectorOperationGetCliCommand : ProfiledCliCommand
{
protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(ConnectorOperationGetCliCommand));

[CliOption(Name = "--connector", Description = "Connector name as returned by 'connector list' (e.g. shared_commondataserviceforapps).", Required = true)]
public string Connector { get; set; } = null!;

[CliOption(Name = "--operation", Description = "Operation id as returned by 'connector operation list' (e.g. CreateRecord).", Required = true)]
public string Operation { get; set; } = null!;

protected override async Task<int> ExecuteAsync()
{
var service = TxcServices.Get<IConnectorCatalogService>();
ConnectorOperationDetail? detail = await service
.GetOperationAsync(Profile, Connector, Operation, CancellationToken.None)
.ConfigureAwait(false);

if (detail is null)
{
Logger.LogError(
"Operation '{Operation}' was not found on connector '{Connector}'. Use 'txc environment connector operation list --connector {Connector}' to see available operations.",
Operation, Connector, Connector);
return ExitValidationError;
}

OutputFormatter.WriteData(detail, PrintDetail);
return ExitSuccess;
}

// Text-renderer callback invoked by OutputFormatter.WriteData — OutputWriter usage is intentional.
#pragma warning disable TXC003
private static void PrintDetail(ConnectorOperationDetail d)
{
const int labelWidth = -14;
OutputWriter.WriteLine($"{"Operation:",labelWidth}{d.OperationId}");
OutputWriter.WriteLine($"{"Connector:",labelWidth}{d.ConnectorName}");
OutputWriter.WriteLine($"{"Api Id:",labelWidth}{d.ApiId}");
OutputWriter.WriteLine($"{"Kind:",labelWidth}{d.Kind}");
OutputWriter.WriteLine($"{"Endpoint:",labelWidth}{d.HttpMethod} {d.Path}");
if (!string.IsNullOrWhiteSpace(d.Summary))
OutputWriter.WriteLine($"{"Summary:",labelWidth}{d.Summary}");

OutputWriter.WriteLine(string.Empty);

if (d.Parameters.Count == 0)
{
OutputWriter.WriteLine("No parameters.");
return;
}

int nameWidth = Math.Clamp(d.Parameters.Max(p => p.Name.Length), 20, 60);
int typeWidth = 18;
string header = $"{"Parameter".PadRight(nameWidth)} | {"In".PadRight(6)} | {"Type".PadRight(typeWidth)} | {"Req".PadRight(3)} | {"Dyn".PadRight(3)} | Summary";
OutputWriter.WriteLine(header);
OutputWriter.WriteLine(new string('-', header.Length));

foreach (var p in d.Parameters)
{
string name = p.Name.Length > nameWidth ? p.Name[..(nameWidth - 1)] + "." : p.Name;
string type = p.Type ?? string.Empty;
type = type.Length > typeWidth ? type[..(typeWidth - 1)] + "." : type;
string summary = p.Summary ?? string.Empty;
if (p.EnumValues is { Count: > 0 })
summary = $"{summary} [{string.Join(", ", p.EnumValues)}]".Trim();
OutputWriter.WriteLine(
$"{name.PadRight(nameWidth)} | {p.In.PadRight(6)} | {type.PadRight(typeWidth)} | {(p.Required ? "yes" : "").PadRight(3)} | {(p.IsDynamic ? "yes" : "").PadRight(3)} | {summary}");
}
}
#pragma warning restore TXC003
}
Loading