diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 227175d03da..6f5fa9d46a6 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -242,6 +242,7 @@
+
diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj
new file mode 100644
index 00000000000..530b9d1bda3
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj
@@ -0,0 +1,43 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
+
diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml
new file mode 100644
index 00000000000..ca7d1eff621
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml
@@ -0,0 +1,30 @@
+#
+# This workflow demonstrates accepting file-based input as workflow input.
+#
+kind: Workflow
+trigger:
+
+ kind: OnConversationStart
+ id: workflow_demo
+ actions:
+
+ # Show that the workflow can inspect the text portion of the message.
+ # The uploaded file is already attached to the conversation and is available
+ # to agent-backed actions that use System.ConversationId.
+ - kind: SendActivity
+ id: announce_file_input
+ activity: |-
+ Received file-based workflow input.
+
+ Prompt:
+ {System.LastMessage.Text}
+
+ # Invoke an agent in the original conversation. The workflow root already added
+ # the file-bearing user message to this conversation before the first action ran.
+ - kind: InvokeAzureAgent
+ id: summarize_file
+ conversationId: =System.ConversationId
+ agent:
+ name: FileInputAgent
+ output:
+ autoSend: true
diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/ProductBrief.txt b/dotnet/samples/03-workflows/Declarative/FileInput/ProductBrief.txt
new file mode 100644
index 00000000000..74777e0bb03
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/FileInput/ProductBrief.txt
@@ -0,0 +1,16 @@
+Product: Contoso Trail Bottle
+
+The Contoso Trail Bottle is a lightweight stainless-steel water bottle designed
+for hikers, commuters, and students. It keeps drinks cold for 24 hours, fits in
+standard backpack side pockets, and uses a leak-resistant twist lid.
+
+Audience:
+- Weekend hikers
+- Urban commuters
+- Students who want a durable reusable bottle
+
+Key differentiators:
+- Recycled stainless-steel body
+- Dishwasher-safe lid
+- Replaceable silicone gasket
+- Optional clip loop for backpacks
diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs
new file mode 100644
index 00000000000..06d09b2385e
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs
@@ -0,0 +1,127 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.AI.Projects;
+using Azure.AI.Projects.Agents;
+using Azure.Identity;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using OpenAI.Files;
+using Shared.Foundry;
+using Shared.Workflows;
+
+namespace Demo.Workflows.Declarative.FileInput;
+
+///
+/// Demonstrate how to provide file-based input to a declarative workflow.
+///
+///
+/// See the README.md file in this folder and the parent folder (../README.md) for
+/// detailed information about the configuration required to run this sample.
+///
+internal sealed class Program
+{
+ public static async Task Main()
+ {
+ // Initialize configuration
+ IConfiguration configuration = Application.InitializeConfig();
+ Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
+
+ // Ensure sample agents exist in Foundry.
+ await CreateAgentAsync(foundryEndpoint, configuration);
+
+ string filePath = Path.Combine(AppContext.BaseDirectory, "ProductBrief.txt");
+ await using UploadedFile uploadedFile = await UploadInputFileAsync(foundryEndpoint, filePath);
+
+ // Create the workflow factory. This class demonstrates how to initialize a
+ // declarative workflow from a YAML file. Once the workflow is created, it
+ // can be executed just like any regular workflow.
+ WorkflowFactory workflowFactory = new("FileInput.yaml", foundryEndpoint);
+
+ // Execute the workflow with a ChatMessage that contains both text and an uploaded
+ // file reference. Agent-backed actions can use the same workflow conversation to
+ // access the file.
+ WorkflowRunner runner = new();
+ await runner.ExecuteAsync(workflowFactory.CreateWorkflow, CreateInputMessage(uploadedFile.FileId));
+ }
+
+ private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration)
+ {
+ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+ // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+ AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());
+
+ await aiProjectClient.CreateAgentAsync(
+ agentName: "FileInputAgent",
+ agentDefinition: DefineFileInputAgent(configuration),
+ agentDescription: "Summarizes files provided as declarative workflow input.");
+ }
+
+ private static DeclarativeAgentDefinition DefineFileInputAgent(IConfiguration configuration) =>
+ new(configuration.GetValue(Application.Settings.FoundryModel))
+ {
+ Instructions =
+ """
+ You summarize files that are provided as user input to a workflow.
+
+ When a file is attached, inspect the file content and provide:
+ - A short summary
+ - Important facts or entities
+ - One suggested follow-up question
+
+ If no file content is available, explain that you did not receive a file.
+ """
+ };
+
+ private static async Task UploadInputFileAsync(Uri foundryEndpoint, string filePath)
+ {
+ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+ // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+ AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());
+ OpenAIFileClient fileClient = aiProjectClient.GetProjectOpenAIClient().GetOpenAIFileClient();
+
+ using FileStream fileStream = File.OpenRead(filePath);
+ OpenAIFile openAIFile = await fileClient.UploadFileAsync(
+ fileStream,
+ Path.GetFileName(filePath),
+ FileUploadPurpose.Assistants).ConfigureAwait(false);
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ try
+ {
+ Console.WriteLine($"FILE: {openAIFile.Id}");
+ }
+ finally
+ {
+ Console.ResetColor();
+ }
+
+ return new UploadedFile(fileClient, openAIFile.Id);
+ }
+
+ private static ChatMessage CreateInputMessage(string fileId)
+ {
+ return new ChatMessage(
+ ChatRole.User,
+ [
+ new TextContent("Summarize the attached file for a launch announcement. File name: ProductBrief.txt"),
+ new HostedFileContent(fileId),
+ ]);
+ }
+
+ private sealed record UploadedFile(OpenAIFileClient FileClient, string FileId) : IAsyncDisposable
+ {
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ await this.FileClient.DeleteFileAsync(this.FileId).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Unable to delete uploaded file {this.FileId}: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/README.md b/dotnet/samples/03-workflows/Declarative/FileInput/README.md
new file mode 100644
index 00000000000..85a6eb490b5
--- /dev/null
+++ b/dotnet/samples/03-workflows/Declarative/FileInput/README.md
@@ -0,0 +1,15 @@
+# Declarative workflow file input
+
+This sample demonstrates how to provide file-based input to a declarative workflow. It uploads the bundled `ProductBrief.txt` file to the Foundry project, converts the uploaded file reference into a `ChatMessage` with both `TextContent` and `HostedFileContent`, then starts a YAML-defined workflow with that message.
+
+The workflow displays `System.LastMessage.Text`, then invokes a Foundry-backed agent in the same workflow conversation so the uploaded file is available to the agent.
+
+## Run the sample
+
+Configure the common declarative workflow settings described in the parent [README](../README.md), then run:
+
+```pwsh
+dotnet run
+```
+
+The sample always uploads `ProductBrief.txt` from this project. The program creates a `ChatMessage` whose content includes the prompt and uploaded file reference, then starts the workflow with that message. The YAML invokes the agent with `conversationId: =System.ConversationId` so the agent sees the same conversation item that contains the file.
diff --git a/dotnet/samples/03-workflows/Declarative/README.md b/dotnet/samples/03-workflows/Declarative/README.md
index 1fe87a6a782..745d4fe122b 100644
--- a/dotnet/samples/03-workflows/Declarative/README.md
+++ b/dotnet/samples/03-workflows/Declarative/README.md
@@ -97,3 +97,15 @@ To run the sampes from the command line:
dotnet run c:/myworkflows/Marketing.yaml
```
> The sample will allow for interactive input in the absence of an input argument.
+
+### File-based input
+
+The `FileInput` sample demonstrates starting a declarative workflow with a `ChatMessage`
+that contains an uploaded file reference, not just text:
+
+```pwsh
+cd dotnet/samples/03-workflows/Declarative/FileInput
+dotnet run
+```
+
+See [FileInput](./FileInput/) for details.
diff --git a/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs
index b5573ec80e1..c0c3f882104 100644
--- a/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs
+++ b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs
@@ -54,6 +54,17 @@ public WorkflowRunner(params IEnumerable functions)
}
public async Task ExecuteAsync(Func workflowProvider, string input)
+ {
+ await this.ExecuteCoreAsync(workflowProvider, input).ConfigureAwait(false);
+ }
+
+ public async Task ExecuteAsync(Func workflowProvider, ChatMessage input)
+ {
+ await this.ExecuteCoreAsync(workflowProvider, input).ConfigureAwait(false);
+ }
+
+ private async Task ExecuteCoreAsync(Func workflowProvider, TInput input)
+ where TInput : notnull
{
// Reset EOF flag so a reused WorkflowRunner instance handles stdin correctly on each run.
this._stdinEof = false;