Business Professionals
Power BI | Power Pivot | Power Query | DAX
Cloud Flows | RPA | AI Builder | Copilot
60+ Formulas | Data Stories | Advanced Reporting & Modeling
VB Programming | Report Automation |
MS-Office Automation
Techno-Business Professionals
Power BI | Power Query | Advanced DAX | SQL - Query &
Programming
Microsoft Fabric | Power BI | Power Query | Advanced DAX |
SQL - Query & Programming
Power BI | Power Apps | Power Automate | Copilot Studio | Power Pages | Dataverse
Microsoft Power Apps | Microsoft Power Automate
Power BI | Adv. DAX | SQL (Query & Programming) |
VBA | Python | Web Scrapping | API Integration
Power BI | Power Apps | Power Automate |
SQL (Query & Programming)
Power BI | Adv. DAX | Power Apps | Power Automate |
SQL (Query & Programming) | VBA | Python | Web Scrapping | API Integration
Power Apps | Power Automate | SQL | VBA | Python |
Web Scraping | RPA | API Integration
Technology Professionals
Power BI | DAX | SQL | ETL with SSIS | SSAS | VBA | Python
Power BI | SQL | Azure Data Lake | Synapse Analytics |
Data Factory | Databricks | Power Apps | Power Automate |
Azure Analysis Services
Microsoft Fabric | Power BI | SQL | Lakehouse |
Data Factory (Pipelines) | Dataflows Gen2 | KQL | Delta Tables | Power Apps | Power Automate
Power BI | Power Apps | Power Automate | SQL | VBA | Python | API Integration
Power BI | Advanced DAX | Databricks | SQL | Lakehouse Architecture
Business Professionals
Power BI | Power Pivot | Power Query | DAX
Cloud Flows | RPA | AI Builder | Copilot
60+ Formulas | Data Stories | Advanced Reporting & Modeling
VB Programming | Report Automation |
MS-Office Automation
Techno-Business Professionals
Power BI | Power Query | Advanced DAX | SQL - Query &
Programming
Microsoft Fabric | Power BI | Power Query | Advanced DAX |
SQL - Query & Programming
Power BI | Power Apps | Power Automate | Copilot Studio | Power Pages | Dataverse
Microsoft Power Apps | Microsoft Power Automate
Power BI | Adv. DAX | SQL (Query & Programming) |
VBA | Web Scrapping | API Integration
Power BI | Power Apps | Power Automate |
SQL (Query & Programming)
Power BI | Adv. DAX | Power Apps | Power Automate |
SQL (Query & Programming) | VBA | Web Scrapping | API Integration
Power Apps | Power Automate | SQL | VBA |
Web Scraping | RPA | API Integration
Technology Professionals
Power BI | DAX | SQL | ETL with SSIS | SSAS | VBA
Power BI | SQL | Azure Data Lake | Synapse Analytics |
Data Factory | Azure Analysis Services
Microsoft Fabric | Power BI | SQL | Lakehouse |
Data Factory (Pipelines) | Dataflows Gen2 | KQL | Delta Tables
Power BI | Power Apps | Power Automate | SQL | VBA | API Integration
Power BI | Advanced DAX | Databricks | SQL | Lakehouse Architecture
LEARN THIS HANDS ON
Power Apps & Power Automate
Most teams already have years of business logic locked away in MS-SQL stored procedures, while Power Apps makers are busy re‑implementing the same rules in formulas. The result: duplicated logic, inconsistent results, and security teams getting nervous about direct table access.
This article shows how to move from stored procedures to Power Apps in a controlled way: exposing existing SQL logic via APIs and connectors so non‑developers can use it, without breaking your security model or maintainability.
Imagine an internal credit team.
Today, the workflow looks like this:
Now the business asks for a Power App:
We want to:
The rest of the article walks through one way to do this safely.
Power Apps does not natively execute arbitrary stored procedures via the built‑in SQL Server connector in a way that covers all scenarios (especially complex parameters and result sets). While the SQL Server connector can call stored procedures that meet its parameter and result-set constraints, it is not suitable for all patterns (for example, procedures that return multiple result sets or require complex table-valued parameters). As of current documentation, a robust and flexible way to expose stored procedures to Power Apps is:
Other options exist (e.g. direct SQL connector queries, Power Automate flows calling SQL), but the API + custom connector pattern gives you:
For our credit limit scenario, we’ll build:
POST /credit/assess endpoint that calls usp_AssessCreditLimit.APIs speak JSON; stored procedures speak T‑SQL. You need a clean mapping between the two.
You don’t have to rewrite your business logic, but you may need a wrapper procedure that:
Example wrapper for our scenario:
CREATE PROCEDURE dbo.usp_AssessCreditLimit
@CustomerId INT,
@RequestedLimit DECIMAL(18, 2),
@RequestedByUserId INT,
@RequestTimestampUtc DATETIME2(0)
AS
BEGIN
SET NOCOUNT ON;
-- Example: call existing complex logic
-- Assume dbo.usp_CalculateCreditScore already exists
DECLARE @CreditScore INT;
EXEC dbo.usp_CalculateCreditScore
@CustomerId = @CustomerId,
@CreditScore = @CreditScore OUTPUT;
DECLARE @CurrentBalance DECIMAL(18, 2);
DECLARE @ApprovedLimit DECIMAL(18, 2);
DECLARE @DecisionCode NVARCHAR(50);
-- Business logic simplified for illustration
SELECT @CurrentBalance = CurrentBalance
FROM dbo.CustomerAccounts
WHERE CustomerId = @CustomerId;
IF @CreditScore < 500
BEGIN
SET @ApprovedLimit = 0;
SET @DecisionCode = N'Rejected_Risk';
END
ELSE IF @RequestedLimit > 100000
BEGIN
SET @ApprovedLimit = 100000;
SET @DecisionCode = N'Requires_Manual_Review';
END
ELSE
BEGIN
SET @ApprovedLimit = @RequestedLimit;
SET @DecisionCode = N'Approved_Auto';
END
SELECT
@CustomerId AS CustomerId,
@RequestedLimit AS RequestedLimit,
@CurrentBalance AS CurrentBalance,
@CreditScore AS CreditScore,
@ApprovedLimit AS ApprovedLimit,
@DecisionCode AS DecisionCode,
@RequestedByUserId AS RequestedByUserId,
@RequestTimestampUtc AS RequestTimestampUtc;
END;
GO
Key points:
SET NOCOUNT ON; avoids extra rowcount messages that can confuse some client libraries.usp_CalculateCreditScore and other internal procedures.You can use any stack that supports HTTP + SQL Server. A common choice is ASP.NET Core.
The API’s job:
Example controller (simplified, but runnable in a typical ASP.NET Core project):
using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace CreditService.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class CreditController : ControllerBase
{
private readonly string _connectionString;
public CreditController(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("CreditDb");
}
public class CreditAssessmentRequest
{
public int CustomerId { get; set; }
public decimal RequestedLimit { get; set; }
public int RequestedByUserId { get; set; }
public DateTime RequestTimestampUtc { get; set; }
}
public class CreditAssessmentResponse
{
public int CustomerId { get; set; }
public decimal RequestedLimit { get; set; }
public decimal CurrentBalance { get; set; }
public int CreditScore { get; set; }
public decimal ApprovedLimit { get; set; }
public string DecisionCode { get; set; }
public int RequestedByUserId { get; set; }
public DateTime RequestTimestampUtc { get; set; }
}
[HttpPost("assess")]
public async Task<ActionResult<CreditAssessmentResponse>> Assess([FromBody] CreditAssessmentRequest request)
{
if (request == null)
{
return BadRequest("Request body is required.");
}
using (var connection = new SqlConnection(_connectionString))
using (var command = new SqlCommand("dbo.usp_AssessCreditLimit", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@CustomerId", request.CustomerId);
command.Parameters.AddWithValue("@RequestedLimit", request.RequestedLimit);
command.Parameters.AddWithValue("@RequestedByUserId", request.RequestedByUserId);
command.Parameters.AddWithValue("@RequestTimestampUtc", request.RequestTimestampUtc);
await connection.OpenAsync();
using (var reader = await command.ExecuteReaderAsync())
{
if (!await reader.ReadAsync())
{
return NotFound("No assessment result returned.");
}
var response = new CreditAssessmentResponse
{
CustomerId = reader.GetInt32(reader.GetOrdinal("CustomerId")),
RequestedLimit = reader.GetDecimal(reader.GetOrdinal("RequestedLimit")),
CurrentBalance = reader.GetDecimal(reader.GetOrdinal("CurrentBalance")),
CreditScore = reader.GetInt32(reader.GetOrdinal("CreditScore")),
ApprovedLimit = reader.GetDecimal(reader.GetOrdinal("ApprovedLimit")),
DecisionCode = reader.GetString(reader.GetOrdinal("DecisionCode")),
RequestedByUserId = reader.GetInt32(reader.GetOrdinal("RequestedByUserId")),
RequestTimestampUtc = reader.GetDateTime(reader.GetOrdinal("RequestTimestampUtc"))
};
return Ok(response);
}
}
}
}
}
This controller:
POST /api/credit/assess.Security considerations:
Once the API is live, you define a custom connector so makers can use it like any other data source.
At a high level:
https://credit-api.contoso.internal/api).assess action: method POST, path /credit/assess, request/response schema.Example OpenAPI fragment (simplified):
{
"openapi": "3.0.1",
"info": {
"title": "Credit Assessment API",
"version": "1.0"
},
"paths": {
"/credit/assess": {
"post": {
"summary": "Assess requested credit limit",
"operationId": "AssessCreditLimit",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreditAssessmentRequest"
}
}
}
},
"responses": {
"200": {
"description": "Assessment result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreditAssessmentResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"CreditAssessmentRequest": {
"type": "object",
"properties": {
"CustomerId": { "type": "integer" },
"RequestedLimit": { "type": "number", "format": "double" },
"RequestedByUserId": { "type": "integer" },
"RequestTimestampUtc": { "type": "string", "format": "date-time" }
},
"required": ["CustomerId", "RequestedLimit", "RequestedByUserId", "RequestTimestampUtc"]
},
"CreditAssessmentResponse": {
"type": "object",
"properties": {
"CustomerId": { "type": "integer" },
"RequestedLimit": { "type": "number", "format": "double" },
"CurrentBalance": { "type": "number", "format": "double" },
"CreditScore": { "type": "integer" },
"ApprovedLimit": { "type": "number", "format": "double" },
"DecisionCode": { "type": "string" },
"RequestedByUserId": { "type": "integer" },
"RequestTimestampUtc": { "type": "string", "format": "date-time" }
}
}
}
}
}
Once imported and configured, the connector appears in Power Apps with an action like CreditAssessmentAPI.AssessCreditLimit.
With the custom connector ready, the maker can wire it into the app.
In our scenario, the app has:
ddlCustomer for selecting the customer.txtRequestedLimit for entering the new limit.btnSubmit to run the assessment.lblDecision to show the decision.Example Power Apps formula for the button’s OnSelect:
Set(
varAssessment,
CreditAssessmentAPI.AssessCreditLimit(
{
CustomerId: Value(ddlCustomer.Selected.CustomerId),
RequestedLimit: Value(txtRequestedLimit.Text),
RequestedByUserId: 0, // map to a numeric ID from your identity system
RequestTimestampUtc: Now()
}
)
);
// Show the decision code in a label
Set(
varDecisionText,
"Decision: " & varAssessment.DecisionCode &
" | Approved limit: " & Text(varAssessment.ApprovedLimit, "#,##0")
);
Notify("Assessment completed.", NotificationType.Success);
Notes:
CreditAssessmentAPI.AssessCreditLimit returns a record shaped like CreditAssessmentResponse.varAssessment in a Dataverse table or another system for audit.This is the after picture for our credit team: requests are submitted and assessed in the app, but the decision logic remains in SQL.
Exposing stored procedures to non‑developers is mostly a security and governance problem, not a technical one.
Focus on these areas:
EXECUTE only on wrapper procedures (usp_AssessCreditLimit, etc.).SELECT/INSERT/UPDATE on business tables unless the procedure requires it and you can’t encapsulate it.RequestedByUserId).Stored procedures evolve. If you change parameters or result sets without coordinating with the API and connector, apps will break.
Practical pattern:
usp_AssessCreditLimit_v2, /credit/assess-v2).Sometimes it’s cleaner to call stored procedures indirectly via Power Automate:
This can be useful when:
However:
For our credit scenario:
The business will ask: "How do we know the Power App is using the same rules as the old SSMS process?"
You need:
Baseline tests
Logging
Monitoring
In our credit team scenario, this is where you convince stakeholders that moving from SSMS to Power Apps hasn’t changed the decision engine, only the interface.
If you remember one thing from this pattern, make it this:
Don’t point Power Apps directly at your core tables or complex stored procedures; wrap them behind a stable, minimal API or Flow contract, then expose that via a custom connector.
It keeps the business logic in MS‑SQL where it belongs, gives non‑developers a safe way to use it, and lets you evolve your database without breaking every app that depends on it.
This article reflects a practical pattern of wrapping existing MS-SQL stored procedures behind APIs and custom connectors so Power Apps makers can safely reuse mature database logic in controlled, governed environments.
Professionals who want to apply these patterns to their own data can explore Excelgoodies' Power Apps & Power Automate programme - taught live by instructors, with certification awarded once a real project is running at work.
Insights compiled through ongoing industry research and discussions within the Excelgoodies Analytics Community.
Power Apps
New
Next Batches Now Live
Power BI
SQL
Power Apps
Power Automate
Microsoft Fabrics
Azure Data Engineering