Skill: Creating .NET Local Functions for Azure Logic Apps Standard
Purpose: This document is a definitive reference for AI agents generating .NET local functions (custom code) that run inside Azure Logic Apps Standard workflows. It contains the exact files, folder structure, NuGet packages, MSBuild targets, VS Code configuration, and workflow invocation patterns taken from verified working projects. Following this document precisely will produce a working project on the first attempt.
Two approaches are documented: .NET Framework 4.7.2 (legacy, in-process) and .NET 8 (modern, recommended). Both use the same workspace layout pattern. Pick one based on your needs.
Table of Contents
- Overview
- Architecture & Workspace Layout (Both Approaches)
- Critical NuGet Package Information
- Approach A — .NET 8 (RECOMMENDED)
- Approach B — .NET Framework 4.7.2 (Legacy)
- Shared Logic App Project Files (Both Approaches)
- Calling Custom Code from workflow.json
- function.json Reference
- Building and Running Locally
- Supported Parameter & Return Types
- Verification Checklists
- Troubleshooting Guide
- Complete Working Examples
- Quick Reference Card
- Step-by-Step Creation Guide for Agents
1. Overview
Logic Apps Standard supports calling custom .NET code directly from workflows using the "Call a local function in this logic app" built-in action (InvokeFunction type). The .NET code runs alongside the Logic Apps runtime — no separate Azure Function hosting is needed.
When to Use
- Porting TIBCO custom Java components, complex Mapper/XSLT logic, or XPath expressions to .NET
- Custom business logic (calculations, transformations, validations)
- XML/JSON document construction using System.Xml or System.Text.Json
- Any scenario where built-in Logic Apps actions/expressions are insufficient
When NOT to Use
- Processes that take more than 10 minutes
- Large message/data transformations (use Data Mapper instead)
- Complex batching/debatching
- TIBCO streaming transformations or large payload processing
Which Approach to Choose
| Factor | .NET Framework 4.7.2 (net472) |
.NET 8 (net8) |
|---|---|---|
| Status | Legacy — still works | Recommended — modern path |
| Worker model | In-process (classic) | Isolated worker |
| Function attribute | [FunctionName("...")] |
[Function("...")] |
| Using for function attr | Microsoft.Azure.WebJobs |
Microsoft.Azure.Functions.Worker |
| Build output folder | lib/custom/net472/ |
lib/custom/net8/ |
| function.json Language | "net472" |
"net8" |
| VS Code customCodeRuntime | "clr" |
"coreclr" |
| Project layout | Sibling Functions/ and LogicApp folders |
Sibling Functions/ and LogicApp folders |
| Best for | Quick ports of existing .NET Framework code | Production projects, CI/CD, new development |
2. Architecture & Workspace Layout (Both Approaches)
CRITICAL: Both .NET 8 and .NET 4.7.2 use the same workspace layout pattern — the Functions project and Logic App project are sibling folders with a
.code-workspacefile at the root. The Functions project is NOT nested inside the Logic App.
2.1 Complete Directory Structure
<workspace-root>/
├── <workspace-name>.code-workspace ← VS Code workspace file
│
├── Functions/ ← .NET function project (sibling)
│ ├── Functions.csproj ← Project file with MSBuild auto-deploy targets
│ ├── <FunctionName>.cs ← One or more C# function files
│ └── .vscode/
│ ├── extensions.json
│ ├── settings.json
│ └── tasks.json
│
└── <logicapp-name>/ ← Logic Apps Standard project (sibling)
├── host.json ← Functions host configuration
├── local.settings.json ← Runtime settings (CRITICAL)
├── .funcignore ← Files to exclude from deployment
├── .gitignore ← Git ignore rules
│
├── .vscode/
│ ├── extensions.json
│ ├── launch.json ← Debug configuration (CRITICAL: customCodeRuntime differs!)
│ ├── settings.json
│ └── tasks.json
│
├── lib/
│ └── custom/ ← Build outputs auto-copied here
│ ├── <FunctionName>/ ← Contains function.json (auto-generated)
│ │ └── function.json
│ └── net8/ OR net472/ ← Contains DLLs (auto-copied by build)
│ ├── Functions.dll
│ ├── Functions.pdb
│ └── (dependency DLLs)
│
├── <workflow-name>/ ← One or more workflow folders
│ └── workflow.json
│
└── workflow-designtime/ ← Design-time configuration
├── host.json
└── local.settings.json
2.2 Key Structural Rules
- Functions/ and LogicApp are siblings — they sit next to each other under the workspace root
- The workspace file references both folders — so VS Code opens them as a multi-root workspace
- Build auto-deploys — MSBuild targets in the
.csprojcopy DLLs andfunction.jsontolib/custom/ lib/custom/is the bridge — DLLs go inlib/custom/net8/orlib/custom/net472/, function metadata goes inlib/custom/<FunctionName>/function.json- workflow-designtime/ is required — it enables the Logic Apps designer to discover custom functions
3. Critical NuGet Package Information
⚠️ THE #1 MISTAKE: Wrong Package Name
| What you might guess | Exists on NuGet? | Correct? |
|---|---|---|
Microsoft.Azure.Functions.Extensions.Workflows |
NO ❌ | ❌ |
Microsoft.Azure.Workflows.WebJobs.Sdk |
YES ✅ | ✅ |
Why this is confusing: The NuGet package ID is Microsoft.Azure.Workflows.WebJobs.Sdk, but the DLL inside the package is named Microsoft.Azure.Functions.Extensions.Workflows.Sdk.dll, and the namespace for the WorkflowActionTrigger attribute is Microsoft.Azure.Functions.Extensions.Workflows. Three different naming conventions for the same thing.
⚠️ THE #2 MISTAKE: Wrong Namespace in Using Statements
| Using statement | Compiles? | Correct? |
|---|---|---|
using Microsoft.Azure.Workflows.WebJobs.Sdk; |
NO ❌ | ❌ |
using Microsoft.Azure.Functions.Extensions.Workflows; |
YES ✅ | ✅ |
Both approaches use the same [WorkflowActionTrigger] attribute from the same namespace. The difference is the function declaration attribute and its associated packages.
Package Verification Command
dotnet package search "Microsoft.Azure.Workflows" --take 5
# Expected result: Microsoft.Azure.Workflows.WebJobs.Sdk
4. Approach A — .NET 8 (RECOMMENDED)
4.1 Project File (Functions/Functions.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<TargetFramework>net8</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Library</OutputType>
<PlatformTarget>AnyCPU</PlatformTarget>
<LogicAppFolderToPublish>$(MSBuildProjectDirectory)\..\my-logicapp</LogicAppFolderToPublish>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<SelfContained>false</SelfContained>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.3.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.15.1" />
<PackageReference Include="Microsoft.Azure.Workflows.Webjobs.Sdk" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
</ItemGroup>
<Target Name="TriggerPublishOnBuild" AfterTargets="Build">
<CallTarget Targets="Publish" />
</Target>
</Project>
4.2 Critical csproj Details — .NET 8
| Property/Element | Value | Why It Matters |
|---|---|---|
<TargetFramework> |
net8 |
Must be net8 — NOT net8.0 (Logic Apps convention) |
<AzureFunctionsVersion> |
v4 |
Required for Azure Functions v4 runtime |
<OutputType> |
Library |
Functions are libraries, not executables |
<PlatformTarget> |
AnyCPU |
Standard target for .NET 8 |
<LogicAppFolderToPublish> |
$(MSBuildProjectDirectory)\..\my-logicapp |
Absolute path to the sibling Logic App folder — the Publish target uses this to know where to deploy outputs. Replace my-logicapp with your Logic App folder name. |
<SelfContained> |
false |
Must NOT be self-contained — Logic Apps runtime provides the host |
TriggerPublishOnBuild target |
Calls Publish AfterTargets Build |
This is the auto-deploy mechanism — on every build, the Publish target automatically copies DLLs to lib/custom/net8/ and function.json to lib/custom/<FunctionName>/ inside the Logic App folder |
IMPORTANT: The .NET 8 approach uses a simple
TriggerPublishOnBuildtarget that callsPublish. The Publish target (from the Worker SDK) handles all file copying automatically. You do NOT need manual copy targets.
4.3 Required Packages — .NET 8
| Package | Version | Why It's Needed |
|---|---|---|
Microsoft.Azure.Functions.Worker.Extensions.Abstractions |
1.3.0 | Isolated worker abstractions |
Microsoft.Azure.Functions.Worker.Sdk |
1.15.1 | Worker SDK — generates function.json and handles Publish |
Microsoft.Azure.Workflows.Webjobs.Sdk |
1.2.0 | Provides [WorkflowActionTrigger] attribute |
Microsoft.Extensions.Logging.Abstractions |
6.0.0 | Provides ILogger<T>, ILoggerFactory |
Microsoft.Extensions.Logging |
6.0.0 | Logging infrastructure |
4.4 Function Code — .NET 8
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
namespace <YourNamespace>
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Extensions.Workflows; // [WorkflowActionTrigger]
using Microsoft.Azure.Functions.Worker; // [Function] — NOT [FunctionName]!
using Microsoft.Extensions.Logging;
/// <summary>
/// Represents the <FunctionName> flow invoked function.
/// </summary>
public class <ClassName>
{
private readonly ILogger<<ClassName>> logger;
public <ClassName>(ILoggerFactory loggerFactory)
{
logger = loggerFactory.CreateLogger<<ClassName>>();
}
/// <summary>
/// Executes the logic app workflow action.
/// </summary>
[Function("<FunctionName>")] // ← [Function], NOT [FunctionName]
public Task<<ResultType>> Run(
[WorkflowActionTrigger] <type> param1, <type> param2)
{
this.logger.LogInformation("Starting <FunctionName>: " + param1);
var result = new <ResultType>()
{
// Set properties...
};
return Task.FromResult(result);
}
/// <summary>
/// Result model — properties become accessible in workflow via @body('ActionName')?['PropertyName']
/// </summary>
public class <ResultType>
{
public <type> Property1 { get; set; }
public <type> Property2 { get; set; }
}
}
}
4.5 Key Using Statements — .NET 8
using Microsoft.Azure.Functions.Extensions.Workflows; // [WorkflowActionTrigger] — same for both approaches
using Microsoft.Azure.Functions.Worker; // [Function("...")] — .NET 8 ONLY
using Microsoft.Extensions.Logging; // ILogger<T>, ILoggerFactory
4.6 Functions/.vscode/ Files — .NET 8
Functions/.vscode/extensions.json
{
"recommendations": ["ms-azuretools.vscode-azurefunctions", "ms-dotnettools.csharp"]
}
Functions/.vscode/settings.json
{
"azureFunctions.deploySubpath": "bin/Release/net8/publish",
"azureFunctions.projectLanguage": "C#",
"azureFunctions.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "publish (functions)",
"azureFunctions.templateFilter": "Core",
"azureFunctions.showTargetFrameworkWarning": false,
"azureFunctions.projectSubpath": "bin\\Release\\net8\\publish"
}
Functions/.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "${config:azureLogicAppsStandard.dotnetBinaryPath}",
"type": "process",
"args": ["build", "${workspaceFolder}"],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
5. Approach B — .NET Framework 4.7.2 (Legacy)
5.1 Project File (Functions/Functions.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<TargetFramework>net472</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Library</OutputType>
<PlatformTarget>x64</PlatformTarget>
<LogicAppFolder>my-logicapp</LogicAppFolder>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Core" Version="3.0.39" />
<PackageReference Include="Microsoft.Azure.Workflows.WebJobs.Sdk" Version="1.1.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
</ItemGroup>
<Target Name="Task" AfterTargets="Compile">
<ItemGroup>
<DirsToClean2 Include="..\$(LogicAppFolder)\lib\custom" />
</ItemGroup>
<RemoveDir Directories="@(DirsToClean2)" />
</Target>
<Target Name="CopyExtensionFiles" AfterTargets="ParameterizedFunctionJsonGenerator">
<ItemGroup>
<CopyFiles Include="$(MSBuildProjectDirectory)\bin\$(Configuration)\net472\**\*.*" CopyToOutputDirectory="PreserveNewest" Exclude="$(MSBuildProjectDirectory)\bin\$(Configuration)\net472\*.*" />
<CopyFiles2 Include="$(MSBuildProjectDirectory)\bin\$(Configuration)\net472\*.*" />
</ItemGroup>
<Copy SourceFiles="@(CopyFiles)" DestinationFolder="..\$(LogicAppFolder)\lib\custom\%(RecursiveDir)" SkipUnchangedFiles="true" />
<Copy SourceFiles="@(CopyFiles2)" DestinationFolder="..\$(LogicAppFolder)\lib\custom\net472\" SkipUnchangedFiles="true" />
<ItemGroup>
<MoveFiles Include="..\$(LogicAppFolder)\lib\custom\bin\*.*" />
</ItemGroup>
<Move SourceFiles="@(MoveFiles)" DestinationFolder="..\$(LogicAppFolder)\lib\custom\net472" />
<ItemGroup>
<DirsToClean Include="..\$(LogicAppFolder)\lib\custom\bin" />
</ItemGroup>
<RemoveDir Directories="@(DirsToClean)" />
</Target>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<Folder Include="bin\$(Configuration)\net472\" />
</ItemGroup>
</Project>
5.2 Critical csproj Details — .NET 4.7.2
| Property/Element | Value | Why It Matters |
|---|---|---|
<TargetFramework> |
net472 |
.NET Framework 4.7.2 |
<PlatformTarget> |
x64 |
Must be x64 for net472 (NOT AnyCPU) |
<LogicAppFolder> |
my-logicapp |
Relative folder name of the sibling Logic App project. Replace with your Logic App folder name. |
Task target (AfterTargets Compile) |
Cleans lib/custom/ |
Ensures fresh outputs on every build |
CopyExtensionFiles target (AfterTargets ParameterizedFunctionJsonGenerator) |
Copies build outputs | Note: ParameterizedFunctionJsonGenerator (NOT ParameterizedFunctionJsonGeneratorNetCore — that's for .NET 8) |
<Reference Include="Microsoft.CSharp" /> |
Assembly reference | Required for dynamic features |
IMPORTANT: The .NET 4.7.2 approach uses explicit MSBuild targets to clean, copy, and reorganize build outputs. The
CopyExtensionFilestarget runs afterParameterizedFunctionJsonGeneratorwhich is provided by theMicrosoft.NET.Sdk.Functionspackage to auto-generatefunction.jsonfiles.
5.3 How the MSBuild Targets Work (net472)
The targets perform these steps in order:
Tasktarget (after Compile): Deletes../my-logicapp/lib/custom/to start freshCopyExtensionFilestarget (after ParameterizedFunctionJsonGenerator):- Copies subdirectories (e.g.,
Functions/function.json) frombin/$(Configuration)/net472/to../my-logicapp/lib/custom/ - Copies root files (DLLs, PDBs) from
bin/$(Configuration)/net472/to../my-logicapp/lib/custom/net472/ - Moves any stray files from
../my-logicapp/lib/custom/bin/to../my-logicapp/lib/custom/net472/ - Removes the empty
bin/folder
- Copies subdirectories (e.g.,
5.4 Required Packages — .NET 4.7.2
| Package | Version | Why It's Needed |
|---|---|---|
Microsoft.Azure.WebJobs.Core |
3.0.39 | Provides [FunctionName] attribute |
Microsoft.Azure.Workflows.WebJobs.Sdk |
1.1.0 | Provides [WorkflowActionTrigger] attribute |
Microsoft.NET.Sdk.Functions |
4.2.0 | Azure Functions SDK — generates function.json, provides ParameterizedFunctionJsonGenerator target |
Microsoft.Extensions.Logging.Abstractions |
2.1.1 | Provides ILogger<T>, ILoggerFactory |
Microsoft.Extensions.Logging |
2.1.1 | Logging infrastructure |
⚠️ IMPORTANT: Use
Microsoft.Azure.WebJobs.Core(notMicrosoft.Azure.WebJobs). The.Corepackage provides just the attributes without pulling in the full WebJobs host.
5.5 Function Code — .NET 4.7.2
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
namespace <YourNamespace>
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Extensions.Workflows; // [WorkflowActionTrigger]
using Microsoft.Azure.WebJobs; // [FunctionName] — NOT [Function]!
using Microsoft.Extensions.Logging;
/// <summary>
/// Represents the <FunctionName> flow invoked function.
/// </summary>
public class <ClassName>
{
private readonly ILogger<<ClassName>> logger;
public <ClassName>(ILoggerFactory loggerFactory)
{
logger = loggerFactory.CreateLogger<<ClassName>>();
}
/// <summary>
/// Executes the logic app workflow action.
/// </summary>
[FunctionName("<FunctionName>")] // ← [FunctionName], NOT [Function]
public Task<<ResultType>> Run(
[WorkflowActionTrigger] <type> param1, <type> param2)
{
this.logger.LogInformation("Starting <FunctionName>: " + param1);
var result = new <ResultType>()
{
// Set properties...
};
return Task.FromResult(result);
}
/// <summary>
/// Result model — properties become accessible in workflow via @body('ActionName')?['PropertyName']
/// </summary>
public class <ResultType>
{
public <type> Property1 { get; set; }
public <type> Property2 { get; set; }
}
}
}
5.6 Key Using Statements — .NET 4.7.2
using Microsoft.Azure.Functions.Extensions.Workflows; // [WorkflowActionTrigger] — same for both approaches
using Microsoft.Azure.WebJobs; // [FunctionName("...")] — net472 ONLY
using Microsoft.Extensions.Logging; // ILogger<T>, ILoggerFactory
5.7 Functions/.vscode/ Files — .NET 4.7.2
Functions/.vscode/extensions.json
{
"recommendations": ["ms-azuretools.vscode-azurefunctions", "ms-dotnettools.csharp"]
}
Functions/.vscode/settings.json
{
"azureFunctions.deploySubpath": "bin/Release/net472/publish",
"azureFunctions.projectLanguage": "C#",
"azureFunctions.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "publish (functions)",
"azureFunctions.templateFilter": "Core",
"azureFunctions.showTargetFrameworkWarning": false,
"azureFunctions.projectSubpath": "bin\\Release\\net472\\publish"
}
Functions/.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "${config:azureLogicAppsStandard.dotnetBinaryPath}",
"type": "process",
"args": ["build", "${workspaceFolder}"],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
6. Shared Logic App Project Files (Both Approaches)
The following files are identical (or nearly so) for both .NET 8 and .NET 4.7.2. The only difference is noted in launch.json.
6.1 Workspace File (<workspace-name>.code-workspace)
{
"folders": [
{
"name": "my-logicapp",
"path": "./my-logicapp"
},
{
"name": "Functions",
"path": "./Functions"
}
],
"settings": {}
}
Note: The Logic App folder MUST be listed first in the
foldersarray. Thesettingsblock can optionally includeterminal.integrated.env.windowswith a PATH to the Logic Apps DotNetSDK if needed for local development.
6.2 host.json
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
"version": "[1.*, 2.0.0)"
}
}
6.3 local.settings.json (CRITICAL)
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_INPROC_NET8_ENABLED": "1",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"APP_KIND": "workflowapp",
"AzureWebJobsFeatureFlags": "EnableMultiLanguageWorker",
"ProjectDirectoryPath": "<absolute-path-to-logicapp-folder>",
"WORKFLOWS_SUBSCRIPTION_ID": ""
}
}
| Setting | Value | Why |
|---|---|---|
FUNCTIONS_WORKER_RUNTIME |
"dotnet" |
NEVER use "dotnet-isolated" even for .NET 8 local functions |
FUNCTIONS_INPROC_NET8_ENABLED |
"1" |
Enables the .NET 8 host that loads both net472 and net8 assemblies. Required for BOTH approaches. |
APP_KIND |
"workflowapp" |
Identifies this as a Logic App Standard project (note: lowercase 'a' in 'app') |
AzureWebJobsFeatureFlags |
"EnableMultiLanguageWorker" |
Required to enable custom code execution alongside the workflow engine |
AzureWebJobsStorage |
"UseDevelopmentStorage=true" |
Uses Azurite for local development |
ProjectDirectoryPath |
Absolute path to logicapp folder | Tells the runtime where to find workflow definitions |
WORKFLOWS_SUBSCRIPTION_ID |
"" |
Azure subscription ID (empty for local development) |
6.4 launch.json (⚠️ DIFFERS BETWEEN APPROACHES)
For .NET 8:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run/Debug logic app with local function my-logicapp",
"type": "logicapp",
"request": "launch",
"funcRuntime": "coreclr",
"customCodeRuntime": "coreclr",
"isCodeless": true
}
]
}
For .NET 4.7.2:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run/Debug logic app with local function my-logicapp",
"type": "logicapp",
"request": "launch",
"funcRuntime": "coreclr",
"customCodeRuntime": "clr",
"isCodeless": true
}
]
}
⚠️ CRITICAL DIFFERENCE:
customCodeRuntimemust be"coreclr"for .NET 8 and"clr"for .NET 4.7.2. Getting this wrong will cause the custom code to fail at runtime. Both use"funcRuntime": "coreclr".
6.5 .vscode/extensions.json
{
"recommendations": ["ms-azuretools.vscode-azurelogicapps"]
}
6.6 .vscode/settings.json
{
"azureLogicAppsStandard.projectLanguage": "JavaScript",
"azureLogicAppsStandard.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.suppressProject": true
}
Note:
azureLogicAppsStandard.projectLanguageis"JavaScript"even though the custom code is C#. This refers to the Logic Apps workflow engine language, not the custom code language.azureFunctions.suppressProjectmust betrueto prevent the Azure Functions extension from interfering.
6.7 .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "generateDebugSymbols",
"command": "${config:azureLogicAppsStandard.dotnetBinaryPath}",
"args": ["${input:getDebugSymbolDll}"],
"type": "process",
"problemMatcher": "$msCompile"
},
{
"type": "shell",
"command": "${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}",
"args": ["host", "start"],
"options": {
"env": {
"PATH": "${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\DotNetSDK;$env:PATH"
}
},
"problemMatcher": "$func-watch",
"isBackground": true,
"label": "func: host start",
"group": {
"kind": "build",
"isDefault": true
}
}
],
"inputs": [
{
"id": "getDebugSymbolDll",
"type": "command",
"command": "azureLogicAppsStandard.getDebugSymbolDll"
}
]
}
6.8 workflow-designtime/host.json
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
"version": "[1.*, 2.0.0)"
},
"extensions": {
"workflow": {
"settings": {
"Runtime.WorkflowOperationDiscoveryHostMode": "true"
}
}
}
}
Purpose: This enables the Logic Apps designer to discover available custom functions and connectors at design time. Without this, the designer won't show the "Call a local function" action.
6.9 workflow-designtime/local.settings.json
{
"IsEncrypted": false,
"Values": {
"APP_KIND": "workflowapp",
"ProjectDirectoryPath": "<absolute-path-to-logicapp-folder>",
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsSecretStorageType": "Files"
}
}
Note: This file uses
"FUNCTIONS_WORKER_RUNTIME": "node"— this is correct for design-time only. The runtimelocal.settings.jsonuses"dotnet".
6.10 .funcignore
.debug
.git*
.vscode
__azurite_db*__.json
__blobstorage__
__queuestorage__
global.json
local.settings.json
test
workflow-designtime/
6.11 .gitignore
# Azure logic apps artifacts
bin
obj
appsettings.json
local.settings.json
__blobstorage__
.debug
__queuestorage__
__azurite_db*__.json
# Added folders and file patterns
workflow-designtime/
*.code-workspace
6.12 Artifacts/ Folder
Do NOT create Artifacts/ by default as part of the local-functions setup.
Only create Artifacts/Maps/, Artifacts/Rules/, or Artifacts/Schemas/ later if the migrated solution actually needs maps, schemas, HIDX files, or rules.
6.13 lib/builtinOperationSdks/ Folder
Do NOT create lib/builtinOperationSdks/ by default as part of the local-functions setup.
Only create that folder later if runtime/tooling explicitly requires it. If it ever exists, keep it completely empty.
⚠️ CRITICAL — Never put
.gitkeepor any file insidelib/builtinOperationSdks/JAR/orlib/builtinOperationSdks/net472/if those folders are ever created. The Azure Functions runtime detects non-empty folders there and attempts to load Java and .NET Framework workers. If those workers fail (for example, becauseJAVA_HOMEis not set), the .NET 8 worker can also fail to initialize — causingInvokeFunctioncalls to error with "function does not exist".
7. Calling Custom Code from workflow.json
The workflow.json is identical regardless of which .NET approach you use.
7.1 InvokeFunction Action
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Call_a_local_function_in_this_logic_app": {
"type": "InvokeFunction",
"inputs": {
"functionName": "<FunctionName>",
"parameters": {
"param1": "<value_or_expression>",
"param2": "<value_or_expression>"
}
},
"runAfter": {}
}
},
"triggers": {
"When_a_HTTP_request_is_received": {
"type": "Request",
"kind": "Http",
"inputs": {}
}
},
"contentVersion": "1.0.0.0",
"outputs": {}
},
"kind": "Stateful"
}
7.2 Accessing Function Results in Subsequent Actions
"@body('Call_a_local_function_in_this_logic_app')"
To access a specific property of the result:
"@body('Call_a_local_function_in_this_logic_app')?['PropertyName']"
7.3 Key Rules
functionNamemust exactly match the name in[Function("...")]or[FunctionName("...")](case-sensitive)parametersis a flat JSON object — each key maps to a C# method parameter by name- Parameter names are case-sensitive and must match the C# parameter names exactly
- The
typemust be"InvokeFunction"(not"Function"or"AzureFunction") runAftercontrols execution order — use{}for the first action after a trigger
7.4 Example with Previous Action Output as Parameters
{
"Call_a_local_function_in_this_logic_app": {
"type": "InvokeFunction",
"inputs": {
"functionName": "Functions",
"parameters": {
"zipCode": "@triggerBody()?['zipCode']",
"temperatureScale": "@triggerBody()?['scale']"
}
},
"runAfter": {}
},
"Response": {
"type": "Response",
"kind": "http",
"inputs": {
"statusCode": 200,
"body": "@body('Call_a_local_function_in_this_logic_app')"
},
"runAfter": {
"Call_a_local_function_in_this_logic_app": ["Succeeded"]
}
}
}
8. function.json Reference
The function.json file is auto-generated by the build process and placed in lib/custom/<FunctionName>/function.json. You should NOT create this file manually — the MSBuild targets handle it.
8.1 .NET 8 function.json (auto-generated)
{
"Name": "<FunctionName>",
"ScriptFile": "<ProjectName>.dll",
"FunctionDirectory": null,
"EntryPoint": "<Namespace>.<ClassName>.Run",
"Language": "net8",
"Properties": {},
"Bindings": [
{
"Name": "<firstParamName>",
"Connection": null,
"Type": "workflowActionTrigger",
"Properties": {},
"Direction": "In",
"DataType": null,
"Cardinality": null,
"IsTrigger": true,
"IsReturn": false,
"Raw": null
}
],
"InputBindings": [
{
"Name": "<firstParamName>",
"Connection": null,
"Type": "workflowActionTrigger",
"Properties": {},
"Direction": "In",
"DataType": null,
"Cardinality": null,
"IsTrigger": true,
"IsReturn": false,
"Raw": null
}
],
"OutputBindings": [],
"Trigger": {
"Name": "<firstParamName>",
"Connection": null,
"Type": "workflowActionTrigger",
"Properties": {},
"Direction": "In",
"DataType": null,
"Cardinality": null,
"IsTrigger": true,
"IsReturn": false,
"Raw": null
},
"InputSchema": {
"type": "object",
"properties": {
"<param1>": { "type": "<jsonType>" },
"<param2>": { "type": "<jsonType>" }
},
"required": ["<param1>", "<param2>"]
},
"OutputSchema": {
"type": "object",
"properties": {
"<Property1>": { "type": "<jsonType>" },
"<Property2>": { "type": "<jsonType>" }
},
"required": ["<Property1>", "<Property2>"]
}
}
Key fields for .NET 8:
"Name": Set to the function name (not null)"ScriptFile": Just the DLL filename (e.g.,"Functions.dll") — no path prefix"Language":"net8""InputSchema"/"OutputSchema": Auto-generated from parameter and return types
8.2 .NET 4.7.2 function.json (auto-generated)
{
"Name": null,
"ScriptFile": "../bin/<ProjectName>.dll",
"FunctionDirectory": null,
"EntryPoint": "<Namespace>.<ClassName>.Run",
"Language": "net472",
"Properties": {},
"Bindings": [ "..." ],
"InputBindings": [ "...
…(truncated)