Design-time experience

Control how your activities appear in Studio - toolbox, icons, cards, the Properties panel, editors, conditions, validation, translations and design-time data providers.

Everything Studio shows for your activities comes from attributes on the activity classes. When you pack, the build reads them into velophex/activity-design.json, and Studio renders that document with its own controls and theme. You do not need Studio, WPF or a design project for any of it. A design project, built on Velophex.Studio.Sdk, is only for the few things attributes cannot carry: dropdowns filled at design time, and custom editors or card bodies. See Add a design project. The toolbox entry# [ActivityInfo] on the class: Member Shown as When absent DisplayName Toolbox row, card header, Properties header The class name Category, Subcategory The toolbox group, Category › Subcategory Activities Description Tooltip in the toolbox and the Properties header. One or two sentences. None IconKey Toolbox and card icon, from icons/<key>.svg Pack fails (VXMAN008) Keywords (comma-separated text), Aliases (array) Extra toolbox search terms None HelpUri The help link Studio opens with F1 and from the card menu (http or https only) Your help base URL plus the type id, if you set one (see Reference) DefaultDisplayName The name a new node gets, when it should differ from DisplayName DisplayName Hidden = true Not offered in the toolbox; existing workflows still run it Offered CardType The card body: Standard, Compact, Container, Scope, Loop, Decision, Trigger Derived: Scope for a scope's open step, Container for a container, otherwise Standard Categories. Name the task, not the product: Invoices, Mail, Text. Use a subcategory only when one category would be too long to scan. Studio lists its own categories (Control, Error Handling, Variables, Invoke, Logging, Dialogs) first and every other category A to Z; a package cannot change that order. Icons# Every activity names an icon key, and the package ships icons/<key>.svg for it, plus icons/package.svg as the package logo in the Package Manager. Put the files in the icons folder beside the runtime project's source; the file name without .svg is the key. package is reserved for the logo. Icons are untrusted content, so Studio reads them with a strict allow-list. Every icon must meet all of these rules: .svg only, in one flat icons/ folder; at most 16 KiB per icon and 256 KiB for all icons together. Well-formed XML with no DTD. Only the elements svg, g, path, rect, circle, ellipse and polygon (title, desc and metadata are ignored). No line or polyline, no fill="none", no on* attributes, href, style, url(, :// or javascript:. viewBox="0 0 24 24". The root element carries only xmlns, viewBox, width and height. At most three <path> elements, each with an explicit fill-rule (evenodd, or nonzero with counter-wound holes). No transform attribute on any element. Bake rotations, scales and offsets into the path coordinates. Colors: only currentColor and the two VeloPhex palette values, #FCE0C2 (pale fill) and #C76A14 (outline). Any other color is refused. Studio maps these to the light, dark and high-contrast themes. The simplest valid icon is one filled path in currentColor: icons/ValidateInvoice.svgXMLCopy<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" fill-rule="evenodd" d="M4 4 H20 A2 2 0 0 1 22 6 V15 A2 2 0 0 1 20 17 H9 L4 21 V17 A2 2 0 0 1 2 15 V6 A2 2 0 0 1 4 4 Z M6 8 V10 H18 V8 Z M6 12 V14 H14 V12 Z"/></svg> A scope's open step should use your product's icon, so the scope card, the Outline and breadcrumbs show it. Arguments in the Properties panel and on the card# [ArgumentInfo] on each argument property: Member Shown as When absent DisplayName The row label The property name Description The row label's tooltip None Placeholder Example text in the empty editor, such as "INV-000123" A "required" cue for required arguments Order Row order; ties keep declaration order 0 Group The Properties section, such as Options Input or Output, by direction IsAdvanced = true Inside the collapsed Advanced group Shown in its group Placement PropertyPlacement.Card also shows the row on the activity card; Hidden shows it nowhere Properties EditorHint The editor, by key (see Editors) Inferred: check box for bool, dropdown for Choices, otherwise the expression editor DefaultValue The literal a new node starts with, such as 30 or "GET" Empty Choices = ["A", "B"] A dropdown. It suggests values; a literal outside the list is kept and flagged Free text VisibleWhen = "Mode=X" The row is hidden while the condition is false Always visible EnabledWhen = "Mode!=Off" The row is read-only while the condition is false Always enabled Sensitive = true Redacted everywhere, password editor Plain ValueModes Which input modes the editor offers (Expression, Literal, Variable) All three DataSource, DataSourceDependsOn A dropdown filled by a design-time data provider Free text The card. Put only the activity's one essential input on the card with Placement = PropertyPlacement.Card: the value an author changes for this step. Use two only when the step means nothing without both. Never put a boolean, an option, a timeout, an output or anything with a default on the card. More than two card properties is a warning (VXDSN013). Authors can pin more rows themselves. [DependentChoices("Region=EU", "Frankfurt", "Dublin")] offers different choices while a condition holds. Conditions# VisibleWhen, EnabledWhen, [RequiredWhen], [DependentChoices] and [Advice] share one grammar: Argument=Value or Argument!=Value. Argument is another argument of the same activity, and Value is compared with its literal text, ignoring case. Nothing else is allowed. While the driving argument is bound to an expression, Studio cannot decide the condition, so the row stays visible and the rule is skipped. The build refuses a condition that names a missing argument, names its own property, uses a value outside the driver's Choices, or forms a cycle (VXDSN006). Editors# EditorHint takes a key from EditorKeys (namespace Velophex.Workflow.Activities.Design): EditorKeys Key Editor Expression expression The expression box with IntelliSense (the default) Multiline multiline A multi-line text editor Password password Masked input; pair it with Sensitive = true OutputVariable output-variable A variable picker for an output Checkbox checkbox A check box for a bool FilePath file-path A path with a file browse button FolderPath folder-path A path with a folder browse button Choice choice A dropdown over Choices (set for you when Choices is declared) Resource resource The enclosing scope's resource Code code Studio's code editor dialog Target target The UI Automation target editor Any other key fails the pack (VXDSN005) unless your package ships a design assembly with a property editor for it. Validation rules# These rules are checked by Studio on the card and in the Error List, and by the workflow compiler, on literal values. A rule whose input is an expression is skipped at design time. Attribute Rule Code [RequiredArgument] Must be bound VWF1302 [RequiredWhen("Mode=X")] Must be bound while the condition holds VWF1319 [Range(1, 10)] A literal outside the range is an error. Use the int or double overloads. VWF1317 [RegularExpression("^[A-Z]+$")] A literal must match. The pattern must compile (VXDSN008). VWF1318 [MinLength(n)], [MaxLength(n)] String length VWF1317 [MutuallyExclusive("A", "B")] on the class At most one of the arguments may be bound VWF1320 [Advice("Mode=X", "message")] An information or warning line under the row; never blocks. Severity is Info or Warning only (VXDSN015). VWF1325 [Range], [RegularExpression], [MinLength] and [MaxLength] are the standard System.ComponentModel.DataAnnotations attributes. The others are in Velophex.Workflow.Activities.Metadata. Override in one place with a fluent design# When attributes get hard to read, or you want to adjust presentation in one place, add an ActivityDesign<T> in the runtime project, beside the activity. The build runs it once while packing and merges the result into the design document. Studio never loads it. dotnet new velophex-activity --design adds a sample. GetInvoiceStatusDesign.csC#Copyusing Velophex.Workflow.Activities.Design; namespace Contoso.Invoices.Activities; public sealed class GetInvoiceStatusDesign : ActivityDesign<GetInvoiceStatusActivity> { protected override void Configure(ActivityDesignBuilder<GetInvoiceStatusActivity> design) { design.Property(x => x.BaseUrl).Group("Connection"); design.Property(x => x.ApiKey).Group("Connection"); design.Property(x => x.TimeoutSeconds).Advice("TimeoutSeconds=300", "Five minutes is the longest wait this API allows."); } } Builder methods are named after the attribute members they set: .DisplayName(), .Description(), .Placeholder(), .Order(), .Group(), .IsAdvanced(), .Placement(), .Editor() (the one rename, for EditorHint), .VisibleWhen(), .EnabledWhen(), .Choices(), .DependentChoices(), .DataSource(), .Range(), .Pattern(), .MinLength(), .MaxLength(), .RequiredWhen() and .Advice() on a property, and .Aliases(), .DefaultDisplayName(), .Hidden(), .CardType(), .MutuallyExclusive() and .Example() on the activity itself. Keep one design per activity (VXDSN010). If a design throws while packing, the pack fails (VXDSN011). Translations# Ship a flat JSON file per culture in a resources folder beside the runtime project's source, for example resources/de-DE.json. Studio falls back from de-DE to de to your inline text, key by key. Generate the list of keys instead of typing them: PowerShellCopydotnet pack Contoso.Invoices -c Release -o out -p:VelophexWriteLocalizationTemplate=true This writes resources/invariant.json with every key and its current text. Copy it to <culture>.json, translate the values, and delete the keys you do not translate. invariant.json itself is never packed. resources/de-DE.jsonJSONCopy{ "contoso.invoices.validateInvoiceNumber.displayName": "Rechnungsnummer prüfen", "contoso.invoices.validateInvoiceNumber.description": "Prüft, ob eine Rechnungsnummer das Format INV-000000 hat.", "contoso.invoices.validateInvoiceNumber.InvoiceNumber.displayName": "Rechnungsnummer", "contoso.invoices.validateInvoiceNumber.IsValid.displayName": "Ist gültig" } Keys follow the type id: {typeId}.displayName, .defaultDisplayName, .category, .subcategory, .description, and per argument {typeId}.{Argument}.displayName, .description and .placeholder. An unknown key is a warning (VXDSN009). Examples and a reference page# [ActivityExample("Title", Description = "...", Snippet = "...")] on the class documents an example use. To generate a Markdown reference page for your package from what you actually packed (type ids, toolbox paths, risk, capabilities, every property with its rules, and the examples), pack with: PowerShellCopydotnet pack Contoso.Invoices -c Release -o out -p:VelophexActivityDocsDir=docs\reference The page is written to docs\reference\<PackageId>.md under the package project folder, with an anchor per type id, so you can point your help base URL at it. It is not part of the package. Add a design project# Add a design project only for: Design-time data providers: a dropdown filled while the author edits, for example from the Orchestrator the author is signed in to (IDesignDataSource, net10.0). Custom property editors and card bodies drawn with WPF (IDesignExtensionProvider, net10.0-windows). Studio runs design code only when the package is signed by a signer Studio trusts or comes from a package source marked Trusted. Otherwise the package still installs and runs, and Studio presents it from its design document and manifest alone. Create the project# src/Contoso.Invoices.Activities.Design/Contoso.Invoices.Activities.Design.csprojXMLCopy<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <!-- net10.0 for data providers; net10.0-windows with UseWPF for editors and card bodies. --> <TargetFramework>net10.0</TargetFramework> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <!-- Studio supplies the SDK at design time: never copy it into the package. --> <PackageReference Include="Velophex.Studio.Sdk" Version="$(VelophexStudioSdkVersion)" PrivateAssets="all" /> </ItemGroup> </Project> Reference it from the package project with PackageFolder="design", next to the runtime project: package/Contoso.Invoices.Activities.Package/Contoso.Invoices.Activities.Package.csprojXMLCopy<ItemGroup> <ProjectReference Include="..\..\src\Contoso.Invoices.Activities\Contoso.Invoices.Activities.csproj" PrivateAssets="all" PackageFolder="lib" /> <ProjectReference Include="..\..\src\Contoso.Invoices.Activities.Design\Contoso.Invoices.Activities.Design.csproj" PrivateAssets="all" PackageFolder="design" /> </ItemGroup> Rules for the design project: It must not reference the runtime project or any assembly other than the two SDKs and its own private helpers. Name activities by their type id as text. Never put an ActivityDesign<T> in it (VXDSN018); those belong in the runtime project. Exactly one type per design assembly is named by [assembly: DesignCatalog(typeof(...))]. Studio constructs only that type, with its public parameterless constructor, and the constructor must only build data. A design assembly built against a newer Velophex.Studio.Sdk than the Studio that loads it is skipped, and Studio names both versions. A design-time data provider# The activity names a provider key with DataSource, and the arguments whose values the provider needs with DataSourceDependsOn: GetLedgerActivity.cs (runtime project)C#Copy[ArgumentInfo(DisplayName = "Region", Order = 1, Choices = ["EU", "US"], DefaultValue = "EU")] public InArgument<string> Region { get; set; } = new(); [ArgumentInfo(DisplayName = "Ledger", Order = 2, DataSource = "contoso.ledgers", DataSourceDependsOn = ["Region"])] public InArgument<string> Ledger { get; set; } = new(); The design project serves that key: LedgerDataSource.cs (design project)C#Copyusing System.Net; using System.Net.Http.Json; using Velophex.Studio.Sdk.Catalog; using Velophex.Studio.Sdk.Data; [assembly: DesignCatalog(typeof(Contoso.Invoices.Activities.Design.InvoicesDesignDataSource))] namespace Contoso.Invoices.Activities.Design; public sealed class InvoicesDesignDataSource : IDesignDataSource { public IReadOnlyList<IActivityDesignDataProvider> DataProviders { get; } = [new LedgersProvider()]; } public sealed class LedgersProvider : IActivityDesignDataProvider { // The build reads this constant; the activity's DataSource names the same key. public const string ProviderKey = "contoso.ledgers"; public string Key => ProviderKey; public string Noun => "ledgers"; public async Task<IReadOnlyList<DesignChoice>> GetChoicesAsync(DesignDataRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); IOrchestratorDesignConnection orchestrator = request.Services.Orchestrator ?? throw new DesignDataUnavailableException(DesignDataFailure.SignedOut, "Sign in to Orchestrator to list ledgers."); // DependsOn values are text the user typed: escape them. string region = request.DependsOn.GetValueOrDefault("Region", string.Empty); using HttpResponseMessage response = await orchestrator.GetAsync($"assets?search={Uri.EscapeDataString("ledger-" + region)}", cancellationToken); if (response.StatusCode is HttpStatusCode.Unauthorized) { throw new DesignDataUnavailableException(DesignDataFailure.SignedOut, "The Orchestrator session has ended."); } if (response.StatusCode is HttpStatusCode.Forbidden) { throw new DesignDataUnavailableException(DesignDataFailure.PermissionDenied, "No access to list ledgers."); } if (!response.IsSuccessStatusCode) { throw new DesignDataUnavailableException(DesignDataFailure.Unavailable, $"Orchestrator answered {(int)response.StatusCode}."); } Ledger[] ledgers = await response.Content.ReadFromJsonAsync<Ledger[]>(cancellationToken) ?? []; return [.. ledgers.Select(ledger => new DesignChoice(ledger.Name, ledger.Name, ledger.Description))]; } private sealed record Ledger(string Name, string? Description); } What the author sees: Provider outcome Studio shows A list The dropdown. An empty list shows "No ledgers in this workspace" with Refresh. DesignDataUnavailableException with SignedOut "Sign in to Orchestrator to list ledgers" with Sign in DesignDataUnavailableException with PermissionDenied "No access to list ledgers" Unavailable, any other exception, or no answer within 10 seconds "Could not load choices" with Retry A DependsOn argument is empty "Set Region first"; the provider is not called A DependsOn argument is an expression Free text The Orchestrator connection is read-only and scoped: GetAsync takes a relative path under the design-time workspace's assets, queues and automations, and Studio attaches the signed-in user's token. Your code never sees a credential. Return the complete list; Studio does not page. If no provider in the package serves an argument's key, the build warns (VXDSN007) and Studio shows free text. Custom editors and card bodies# A net10.0-windows design project with <UseWPF>true</UseWPF> can name a type that implements IDesignExtensionProvider (namespace Velophex.Studio.Sdk.Editing). Its PropertyEditors (IPropertyEditorProvider: CanEdit, CreateEditor) supply editors for arguments, usually for an EditorHint key of your own, and its CardContent (ICardContentProvider: CanRender, CreateContent) draws a card's body. Studio always draws the card chrome (title, icon, selection) itself. An editor writes back an expression through IPropertyEditorHost, using host.ToLiteral(value) to produce a literal in the project's language. A provider that throws is reported in the Output panel and skipped, so Studio falls back to its built-in editor. The same type can implement IDesignDataSource too. See your changes in Studio# Studio caches a package's design metadata per package id and version, and a published version must never change. So every change, even a new display name, is a new version: bump the version, pack, and update the installed version in Studio's Package Manager. Repacking the same version leaves Studio showing the old metadata. See Package, version and sign. Next steps# The activity manifest Test activities

The toolbox entry

Icons

Arguments in the Properties panel and on the card

Conditions

Editors

Validation rules

Override in one place with a fluent design

Translations

Examples and a reference page

Add a design project

Create the project

A design-time data provider

Custom editors and card bodies

See your changes in Studio

Next steps