Stop making users leave the record to trigger their most-needed actions. Learn how to add custom buttons, write dynamic visibility rules, and call Power Automate flows directly from the model-driven app command bar using Power Fx — no JavaScript required.

Picture this: your sales team is working in a model-driven app built on Dataverse, and every time they want to send a proposal, they have to leave the Opportunity record, open a separate Canvas app, find the record again, and manually trigger a flow. Meanwhile, the built-in command bar sits there with buttons nobody uses — Merge, Share, Email a Link — taking up prime real estate while the one action everyone needs every day is buried three clicks deep.
The command bar is the strip of action buttons that runs across the top of every model-driven form, view, and subgrid. Until relatively recently, customizing it required either writing JavaScript and XML ribbon definitions by hand, or using third-party tools like Ribbon Workbench. Microsoft changed that in 2022 with the introduction of Power Fx-based command customization in the modern command designer. Now you can add custom buttons, write visibility rules, and trigger Power Automate flows — all inside a point-and-click experience backed by the same formula language you use in Canvas apps.
By the end of this lesson, you'll have a complete, working command bar customization that includes a context-aware custom button, dynamic visibility logic, and a flow trigger — built without writing a single line of JavaScript. Here's exactly what you'll cover:
What you'll learn:
OnSelect and Visible formulas that respond to record state and user contextThis lesson assumes you're comfortable with model-driven apps and Dataverse at an intermediate level. Specifically, you should have:
If your data model is still in early stages, the concepts in Designing a Dataverse Data Model: Relationships, Lookups, and Choice Columns will give you the foundation to follow the examples here.
Before you start customizing, you need to understand what you're actually working with. Model-driven app command bars are not a single thing — they're surfaced in four distinct locations, each with its own set of commands:
Each of these is customizable independently. A button you add to the main form doesn't automatically appear on the main grid. This separation is actually useful — you want different actions available depending on context. "Send Proposal" makes sense on an individual Opportunity form but would be meaningless (or dangerous) as a bulk action on the grid.
Key insight
When you customize commands, you're customizing them at the table + location level, not globally. The same table can have completely different command bars on its form, its view, and any subgrid where it appears. Plan your command strategy for each surface separately.
The modern command designer stores customizations in the AppModule component — meaning your changes are scoped to the specific app you're editing, not the entire Dataverse environment. This is a significant improvement over classic ribbon customization, which applied changes environment-wide and made it dangerously easy to break other apps.
The command designer lives inside Power Apps Studio, accessed through the app editor. Here's how to get there:
Open make.powerapps.com and navigate to Apps. Find your model-driven app and select Edit to open it in the designer. In the left navigation panel, you'll see a tree view of your app's components. Find the table you want to customize — say, Opportunities — and expand it. You'll see entries for Forms, Views, and Commands.
Click on Commands under your table. The right panel will show three tabs: Main form, Main grid, and Subgrid. Select the surface you want to customize. The command bar preview renders inline, showing you the current buttons exactly as users see them.
Note
If you don't see a Commands node in the tree, make sure you're in the app editor for a model-driven app, not a Canvas app. Also confirm the table is added to your app's site map — tables that aren't part of the app navigation won't appear as customizable here.
At this point you're looking at the live command bar for that table and surface. Most buttons you see here are system commands — the built-in actions like Save, Delete, Share, and Refresh. You can hide system commands, but you can't directly modify their behavior. Your custom commands sit alongside them.
Let's add a "Send Proposal" button to the Main form command bar for the Opportunity table. Click the + New command button in the toolbar above the command bar preview. A new unnamed button appears, selected and highlighted.
In the right-side property panel, configure these fields:
Send ProposalSend and select the paper plane iconSend Proposal to ContactGenerates and emails the proposal document for this opportunityThe label is what users see on the button. Keep it short — two words maximum before the command bar starts collapsing buttons into an overflow menu on smaller screens.
Now comes the important part: the Action property. This is where you define what happens when a user clicks the button. You have two choices:
For now, let's start with a formula to understand the mechanics, then escalate to a flow.
Click Run a formula. An OnSelect formula bar appears. Enter this:
Notify("Send Proposal feature coming soon!", NotificationType.Information)
Save the app and publish it. Open an Opportunity record in the published app, and you'll see "Send Proposal" in the command bar. Clicking it shows the notification banner. It's not useful yet, but you've confirmed the wiring works. This validate-then-build approach saves significant debugging time.
The Power Fx environment inside command formulas gives you access to a special object: Self. This refers to the record currently displayed in the form — the Opportunity the user has open right now.
Here's what that enables. Instead of that placeholder notification, you can write logic that actually reads from the record:
If(
IsBlank(Self.crb7a_primarycontact),
Notify(
"Please add a Primary Contact before sending a proposal.",
NotificationType.Warning
),
Notify(
"Sending proposal to " & Self.crb7a_primarycontact.'Full Name' & "...",
NotificationType.Success
)
)
This formula checks whether the Opportunity has a primary contact lookup populated. If it's blank, it warns the user. If the contact exists, it confirms who the proposal will go to. The field name crb7a_primarycontact is the schema name of your lookup column — you'll see it autocomplete in the formula bar as you type.
Tip
Schema names in Power Fx command formulas follow the same pattern as everywhere in Dataverse: publisherprefix_fieldname. If you're unsure of a column's schema name, open the table in the Power Apps maker portal under Tables, find the column, and check its Name property. Copy-paste it to avoid typos.
You can also navigate to related records. If your Opportunity has a lookup to an Account, you can traverse that relationship:
Notify(Self.parentaccountid.'Account Name', NotificationType.Information)
The dot notation traverses the lookup automatically — no need to write a separate Lookup() call. This keeps formulas readable.
Power Fx in command bars also exposes User(), which returns the currently signed-in user's email, full name, and display name. This is useful for logging or conditional logic:
If(
User().Email = Self.'Owner'.'Internal Email Address',
Notify("You own this record.", NotificationType.Success),
Notify("You are not the owner of this opportunity.", NotificationType.Warning)
)
This example checks whether the logged-in user is the record owner before allowing a sensitive action. You'd pair this with a Visible formula (covered next) to actually hide the button rather than just warn — but during development, visible warnings make debugging faster.
Static buttons that are always visible create noise. A "Close as Won" button that appears on already-closed opportunities is confusing and unprofessional. Power Fx Visible formulas let you show and hide buttons dynamically based on record state, field values, or user identity.
In the command designer, select your custom button. Below the OnSelect formula bar, you'll see a Visible property with its own formula bar. By default it's set to true. Change it to a formula:
Self.statuscode = 'Opportunity Status'.'In Progress'
This formula returns true only when the Opportunity's status reason is "In Progress," hiding the button on Won, Lost, and any other status. The 'Opportunity Status' syntax is how Power Fx references Choice column values — use the display name wrapped in single quotes if it contains spaces.
Warning
Visible formulas are evaluated on the client — they control UI display only, not security. A determined user with the right security role could still invoke the underlying flow or action through other means. If the action is sensitive, enforce authorization inside the flow itself or via Dataverse security roles. Never rely on button visibility as your only access control.
Here's a more realistic visibility formula combining multiple conditions:
And(
Self.statuscode = 'Opportunity Status'.'In Progress',
!IsBlank(Self.crb7a_primarycontact),
Self.estimatedvalue >= 10000
)
This shows the "Send Proposal" button only when the opportunity is in-progress, has a contact assigned, and has an estimated value of at least $10,000. These business rules encode real workflow logic — only fully-qualified opportunities of meaningful size should trigger the proposal process.
You can also control the visibility of built-in system commands. Select any system button in the command bar preview — say, Share — and set its Visible formula to false. This effectively removes it from the UI for all users of this app, without affecting other apps or system behavior.
This is one of the most practically valuable things you can do with the command designer. Most organizations have no use for Email a Link, Word Templates (when they're not set up), or Merge on most tables. Stripping out the clutter dramatically improves the user experience.
Tip
Use false conservatively for system commands that might be needed in edge cases. Consider wrapping it in a role check instead of hard-coding false. For example, hide Delete for non-admin users but keep it for System Administrators.
The real power of command customization emerges when you connect buttons to flows. This is the pattern that replaces the "leave the app, open another tool, come back" workflow described in the introduction.
First, build your Power Automate flow. Navigate to make.powerautomate.com and create a new Instant cloud flow. Name it Send Opportunity Proposal.
For the trigger, select Power Apps (V2). This trigger type supports structured inputs — your command button can pass the record's GUID, field values, or any other data the flow needs.
In the trigger, add these inputs:
OpportunityId — TextContactEmail — TextOpportunityName — TextThe flow body can do whatever your proposal process requires — generate a Word document from a template, send an email via Outlook, update the record status, or call an external API. For this example, the flow sends an email:
Add a Send an email (V2) action:
ContactEmail (dynamic content from trigger)Proposal: + OpportunityNameAdd a Respond to a PowerApp or flow action at the end:
ProposalStatus — Text — set it to "Sent"This response lets your command formula know the flow completed successfully so you can update the UI accordingly.
Save and publish the flow.
Back in the command designer, select your "Send Proposal" button and change the action from Run a formula to Run a flow. A dropdown appears — search for and select Send Opportunity Proposal.
Once you select the flow, a formula editor appears asking you to map the flow's input parameters. Map them like this:
OpportunityId: Text(Self.opportunityid)
ContactEmail: Self.crb7a_primarycontact.'Email Address 1'
OpportunityName: Self.'Opportunity Name'
Self.opportunityid is the GUID of the current record. The Text() wrapper converts it to a string that the flow's Text parameter accepts. The other two fields traverse lookups to pull values from related records.
Key insight
When you switch from "Run a formula" to "Run a flow," you lose the ability to write arbitrary Power Fx in OnSelect. The action becomes the flow invocation itself. If you need pre- or post-invocation logic (like a confirmation dialog), wrap the flow call inside a Confirm() pattern — but as of current platform capability, the command designer doesn't yet support Confirm() natively. Use your Visible formula and tooltip text to set expectations instead.
One shortcoming of the current command designer is that there's no native loading state for a button while a flow executes. The user clicks, nothing visibly happens for a few seconds while the flow runs, and then (if you've set up the response) a notification appears. Here's how to handle this gracefully:
After configuring the flow action, Power Apps automatically shows a notification when the flow completes. But you can also use the flow's Respond to a PowerApp or flow action's output in a follow-up formula. As of the current release, command buttons don't have a true OnFlowComplete handler, but you can configure the flow to write back to Dataverse directly — for example, updating a crb7a_proposalstatus field on the Opportunity to "Sent" — and then the form auto-refreshes to reflect the change.
Let's bring everything together into a complete, production-grade command bar customization for an Opportunity table. You'll add three commands:
Send ProposalSendSend Opportunity ProposalAnd(
Self.statuscode = 'Opportunity Status'.'In Progress',
!IsBlank(Self.crb7a_primarycontact),
Self.estimatedvalue >= 10000
)
Sometimes an opportunity goes quiet without being formally lost. This button sets a custom "Stalled" status and logs a note.
Mark StalledPausePatch(
Opportunities,
Self,
{
crb7a_dealstatus: 'Deal Status'.Stalled,
crb7a_stalleddate: Today()
}
);
Notify(
"Opportunity marked as stalled. Follow up scheduled.",
NotificationType.Warning
)
And(
Self.statuscode = 'Opportunity Status'.'In Progress',
Self.crb7a_dealstatus <> 'Deal Status'.Stalled
)
This uses Patch() to update the record in place — the same function you'd use in a Canvas app. It writes two fields and then shows a notification. The form refreshes automatically after a Patch.
Note
Patch() in command formulas operates on the Dataverse table directly. You reference the table by its display name: Patch(Opportunities, Self, {...}). The record being patched is Self, so you don't need to look it up — it's already in scope. This is significantly cleaner than doing the same thing in JavaScript.
Select the built-in Share command:
falseRepeat for Email a Link and Word Templates if they're not in use. You can always restore these by deleting the visibility customization.
Work through this exercise to cement the concepts. Use your own environment and an existing table, or create a simple Project table with columns for Status (Choice: Active, On Hold, Completed), Owner (Lookup to User), and Budget (Currency).
Exercise: Build a Project Status Command Bar
Open your model-driven app in the designer. Navigate to the Project table's Commands > Main form.
Add a custom command called Put On Hold with a pause icon. Write an OnSelect formula that patches the record's Status to "On Hold" and shows a notification confirming the action. Write a Visible formula that shows this button only when Status is "Active."
Add a second command called Reactivate with a play icon. It should patch Status back to "Active" and show a notification. Its Visible formula should show it only when Status is "On Hold."
Add a third command called Mark Complete that patches Status to "Completed." Show it only when Status is "Active" and Budget is greater than zero.
Hide the built-in Share and Email a Link system commands.
Publish the app and test all three buttons on records in different states. Confirm that only the contextually appropriate button appears for each record.
Stretch goal: Create a Power Automate flow that sends an email notification to the record owner when a project is marked complete. Connect it to the Mark Complete button using the Run a flow action instead of Patch. Have the flow update the status (rather than doing it in the formula), so the form refresh comes from the Dataverse write.
Power Fx formula errors in the command designer can be cryptic. The formula bar underlines errors in red, but the message sometimes refers to internal names. Here's the debugging workflow:
Start by clicking the red underline — a tooltip explains the specific issue. The most common errors are:
Text().Check these in order:
Visible formula reference a field that the current user doesn't have column-level security access to? If the formula can't read a restricted field, it may evaluate unexpectedly. See Column-Level Security and Record Sharing in Dataverse for context.The form doesn't automatically refresh after a flow runs unless the flow writes back to Dataverse. If your flow calls an external API but doesn't update the Dataverse record, you won't see any change in the UI. Add a Update a row action to the flow that touches at least one field on the record — even a "Last Proposal Sent" date field — to trigger the automatic form refresh.
The syntax is nearly identical, but there's one key difference: in Canvas apps you typically reference the data source by a global variable or connection name. In command formulas, use the table's display name as it appears in the environment — usually the plural form like Opportunities or Projects. If you're getting a "Name isn't valid" error, check the exact display name in the Power Apps maker portal under Tables.
This usually means the user lacks the security role permissions to either invoke the flow or patch the record. The button appears (because Visible is true) but the action silently fails. To debug:
Warning
When a flow fails silently, users often just click the button again, creating duplicate runs or duplicate data. Add a confirmation step in your flow that writes a status field immediately before the main action. This way, the second click will either be blocked by your Visible formula (if it checks that field) or at minimum you'll see duplicate run history that alerts you to the problem.
Subgrid command bars are customized at the parent table's level, not the child table's level. If you have a Products subgrid on an Opportunity form, you customize its commands by going to the Opportunity table's Commands > Subgrid tab. The subgrid command context gives you access to selected rows via a collection rather than a single Self record.
Power Fx Visible formulas run on every form load and whenever the record refreshes. Keep them simple. A formula that reads five related records from two tables to determine visibility will noticeably slow form load. The design principle is:
Visible formulas should only reference fields on the current record (via Self) or user identitySelfFor example, instead of:
CountRows(
Filter(ProposalLines, ProposalLines.opportunityid = Self.opportunityid)
) > 0
...calculate a crb7a_hasproposallines boolean formula column in Dataverse and reference it:
Self.crb7a_hasproposallines = true
The first formula fires a live query on every form render. The second reads a pre-computed value stored on the row — orders of magnitude faster.
Also consider your button count. Every button in the command bar — visible or hidden — contributes to the evaluation workload. Aim for no more than 5-7 custom buttons per surface, and be ruthless about cleaning up obsolete ones.
The modern command bar designer fundamentally changes what's possible without code in model-driven apps. In this lesson you've learned:
OnSelect formulas that read from the current record via Self, patch data directly, and call Power Automate flowsVisible formulas that dynamically show and hide buttons based on record state, field values, and user identityThe command bar sits at the intersection of UX and workflow — it's the place where users discover what actions are available to them. Getting it right means users can complete their work without leaving the record, without memorizing processes, and without making mistakes. That's worth the investment.
Where to go from here:
The biggest unlock after this lesson is combining what you know about command bars with solid security role design. Make sure the actions you surface are backed by appropriate permissions — the Dataverse Security: Business Units, Security Roles, and Teams article covers that in depth.