diff --git a/libraries/Microsoft.Bot.Builder/EventFactory.cs b/libraries/Microsoft.Bot.Builder/EventFactory.cs
new file mode 100644
index 0000000000..c28fc966c2
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder/EventFactory.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Bot.Schema;
+
+namespace Microsoft.Bot.Builder
+{
+ ///
+ /// Contains utility methods for creating various event types.
+ ///
+ public static class EventFactory
+ {
+ ///
+ /// Create handoff initiation event.
+ ///
+ /// turn context.
+ /// agent hub-specific context.
+ /// transcript of the conversation.
+ /// handoff event.
+ public static IEventActivity CreateHandoffInitiation(ITurnContext turnContext, object handoffContext, Transcript transcript = null)
+ {
+ if (turnContext == null)
+ {
+ throw new ArgumentNullException(nameof(turnContext));
+ }
+
+ var handoffEvent = CreateHandoffEvent(HandoffEventNames.InitiateHandoff, handoffContext, turnContext.Activity.Conversation);
+
+ handoffEvent.From = turnContext.Activity.From;
+ handoffEvent.RelatesTo = turnContext.Activity.GetConversationReference();
+ handoffEvent.ReplyToId = turnContext.Activity.Id;
+ handoffEvent.ServiceUrl = turnContext.Activity.ServiceUrl;
+ handoffEvent.ChannelId = turnContext.Activity.ChannelId;
+
+ if (transcript != null)
+ {
+ var attchment = new Attachment
+ {
+ Content = transcript,
+ ContentType = "application/json",
+ Name = "Transcript",
+ };
+ handoffEvent.Attachments.Add(attchment);
+ }
+
+ return handoffEvent;
+ }
+
+ ///
+ /// Create handoff status event.
+ ///
+ /// Conversation being handed over.
+ /// State, possible values are: "accepted", "failed", "completed".
+ /// Additional message for failed handoff.
+ /// handoff event.
+ public static IEventActivity CreateHandoffStatus(ConversationAccount conversation, string state, string message = null)
+ {
+ if (conversation == null)
+ {
+ throw new ArgumentNullException(nameof(conversation));
+ }
+
+ if (state == null)
+ {
+ throw new ArgumentNullException(nameof(state));
+ }
+
+ object value;
+
+ if (string.IsNullOrEmpty(message))
+ {
+ value = new { state };
+ }
+ else
+ {
+ value = new { state, message };
+ }
+
+ var handoffEvent = CreateHandoffEvent(HandoffEventNames.HandoffStatus, value, conversation);
+ return handoffEvent;
+ }
+
+ private static Activity CreateHandoffEvent(string name, object value, ConversationAccount conversation)
+ {
+ var handoffEvent = Activity.CreateEventActivity() as Activity;
+
+ handoffEvent.Name = name;
+ handoffEvent.Value = value;
+ handoffEvent.Id = Guid.NewGuid().ToString();
+ handoffEvent.Timestamp = DateTime.UtcNow;
+ handoffEvent.Conversation = conversation;
+ handoffEvent.Attachments = new List();
+ handoffEvent.Entities = new List();
+ return handoffEvent;
+ }
+ }
+}
diff --git a/libraries/Microsoft.Bot.Builder/HandoffEventNames.cs b/libraries/Microsoft.Bot.Builder/HandoffEventNames.cs
new file mode 100644
index 0000000000..cedfb74bb8
--- /dev/null
+++ b/libraries/Microsoft.Bot.Builder/HandoffEventNames.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Schema
+{
+ ///
+ /// Defines values for handoff event names.
+ ///
+ public static class HandoffEventNames
+ {
+ public const string InitiateHandoff = "handoff.initiate";
+ public const string HandoffStatus = "handoff.status";
+ }
+}
diff --git a/tests/Microsoft.Bot.Builder.Tests/EventFactoryTests.cs b/tests/Microsoft.Bot.Builder.Tests/EventFactoryTests.cs
new file mode 100644
index 0000000000..80848d4e7b
--- /dev/null
+++ b/tests/Microsoft.Bot.Builder.Tests/EventFactoryTests.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.Builder.Adapters;
+using Microsoft.Bot.Schema;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+
+namespace Microsoft.Bot.Builder.Tests
+{
+ [TestClass]
+ [TestCategory("Message")]
+ public class EventFactoryTests
+ {
+ public TestContext TestContext { get; set; }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void HandoffInitiationNullTurnContext()
+ {
+ EventFactory.CreateHandoffInitiation(null, "some text");
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void HandoffStatusNullConversation()
+ {
+ EventFactory.CreateHandoffStatus(null, "accepted");
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void HandoffStatusNullStatus()
+ {
+ EventFactory.CreateHandoffStatus(new ConversationAccount(), null);
+ }
+
+ [TestMethod]
+ public void TestCreateHandoffInitiation()
+ {
+ var adapter = new TestAdapter(TestAdapter.CreateConversation(TestContext.TestName));
+ string fromID = "test";
+ var activity = new Activity
+ {
+ Type = ActivityTypes.Message,
+ Text = string.Empty,
+ Conversation = new ConversationAccount(),
+ Recipient = new ChannelAccount(),
+ From = new ChannelAccount(fromID),
+ ChannelId = "testchannel",
+ ServiceUrl = "http://myservice"
+ };
+ var context = new TurnContext(adapter, activity);
+
+ var transcript = new Transcript(new Activity[] { MessageFactory.Text("hello") });
+
+ Assert.IsNull(transcript.Activities[0].ChannelId);
+ Assert.IsNull(transcript.Activities[0].ServiceUrl);
+ Assert.IsNull(transcript.Activities[0].Conversation);
+
+ var handoffEvent = EventFactory.CreateHandoffInitiation(context, new { Skill = "any" }, transcript);
+ Assert.AreEqual(handoffEvent.Name, HandoffEventNames.InitiateHandoff);
+
+ Assert.AreEqual(handoffEvent.From.Id, fromID);
+ }
+
+ [TestMethod]
+ public void TestCreateHandoffStatus()
+ {
+ var state = "failed";
+ var message = "timed out";
+ var handoffEvent = EventFactory.CreateHandoffStatus(new ConversationAccount(), state, message);
+ Assert.AreEqual(handoffEvent.Name, HandoffEventNames.HandoffStatus);
+ string status = JsonConvert.SerializeObject(handoffEvent.Value, Formatting.None);
+ Assert.AreEqual(status, $"{{\"state\":\"{state}\",\"message\":\"{message}\"}}");
+ Assert.IsNotNull((handoffEvent as Activity).Attachments);
+ Assert.IsNotNull(handoffEvent.Id);
+ }
+ }
+}