A robust, event-driven action system for KamiYomu that enables extensible automation through triggered actions at key application moments.
Actions are discrete operations triggered by KamiYomu at significant moments during application execution. They enable you to:
- ✅ Automate tasks in response to system events
- ✅ Create extensible workflows without modifying core code
- ✅ Chain multiple actions together for complex automation
- ✅ Respond to manga and chapter lifecycle events
- ✅ Build custom integrations and notifications
Actions can be triggered from various sources throughout the KamiYomu lifecycle:
| Trigger | Description |
|---|---|
| Manual | User-initiated action execution |
| Chained | Triggered by the completion of another action |
| Manga Download | When a manga series is successfully downloaded |
| Chapter Discovery | When new chapters are discovered for a series |
| Chapter Download | When a chapter is successfully downloaded |
| Chapter Page Read | When a user reads a chapter page |
| None | Unspecified or unknown trigger (fallback) |
The KamiYomu.ActionAgents.Core library provides a lightweight, extensible framework for:
- ✅ Defining custom actions that respond to application events
- ✅ Managing action execution and lifecycle
- ✅ Creating complex automation pipelines
- ✅ Decoupling business logic from event handling
IActionAgent: The main interface your action must implementAbstractActionAgent: Base class with common functionality (logging, options, versioning)ActionAgentContext: Provides access to manga, chapter, and trigger source information
Create a new class library targeting .NET 8.0:
dotnet new classlib -n [DeveloperName].ActionAgents.[ProductName] -f net8.0
cd [DeveloperName].ActionAgents.[ProductName]Install the KamiYomu.ActionAgents.Core NuGet package:
dotnet add package KamiYomu.ActionAgents.CoreCreate a new file (e.g., MyFirstActionAgent.cs) with a class implementing IActionAgent:
using KamiYomu.ActionAgents.Core;
using KamiYomu.ActionAgents.Core.Contexts;
using Microsoft.Extensions.Logging;
namespace YourName.ActionAgents.MyAction;
public class MyFirstActionAgent : AbstractActionAgent, IActionAgent
{
public MyFirstActionAgent(IDictionary<string, object> options) : base(options)
{
}
public async Task ExecuteAsync(
ActionAgentContext context,
IDictionary<string, object> options,
CancellationToken cancellationToken)
{
// Receive context information
var mangaTitle = context.Manga?.Title ?? "Unknown Manga";
var chapterNumber = context.Chapter?.Number ?? "Unknown Chapter";
var triggerSource = context.TriggerContext?.Source.ToString() ?? "Unknown";
Logger?.LogInformation($"Action triggered for {mangaTitle}, Chapter {chapterNumber}");
Logger?.LogInformation($"Triggered by: {triggerSource}");
// Perform your action here
await Task.Delay(100, cancellationToken);
Logger?.LogInformation("Action completed successfully!");
}
}Update your .csproj file to make the package discoverable by KamiYomu:
<PropertyGroup>
<PackageTags>kamiyomu;kamiyomu-action-agents;actions;MyAction;</PackageTags>
</PropertyGroup>
<!-- Optional: Auto-generate NuGet package on Debug builds -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>dotnet buildThe ActionAgentContext provides access to relevant information when your action is triggered:
var mangaTitle = context.Manga?.Title;
var mangaUrl = context.Manga?.Url;
var mangaDescription = context.Manga?.Description;var chapterNumber = context.Chapter?.Number;
var chapterUrl = context.Chapter?.URL;
var chapterReleaseDateUtc = context.Chapter?.ReleaseDateUtc;var triggerSource = context.TriggerContext?.Source; // See Action Triggers table above
var triggeredAtUtc = context.TriggerContext?.TriggeredAtUtc;dotnet pack -c ReleaseThis generates a .nupkg file in the bin/Release/ directory.
Choose one of the following distribution methods:
dotnet nuget push bin/Release/YourName.ActionAgents.MyAction.*.nupkg \
--api-key YOUR_NUGET_API_KEY \
--source https://api.nuget.org/v3/index.jsondotnet nuget push bin/Release/YourName.ActionAgents.MyAction.*.nupkg \
--api-key YOUR_GITHUB_TOKEN \
--source https://nuget.pkg.github.com/YourOrg/index.json- Configure your credentials in Visual Studio or NuGet.config
- Push using the standard NuGet push command with your feed URL
- Place the
.nupkgfile directly in KamiYomu's agent folder - Useful for rapid development and testing
Add this to your .csproj to automatically generate a NuGet package on Debug builds:
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>-
Generate Debug Symbols
- Build your project in Debug mode:
dotnet build - This creates
.pdbfiles alongside your DLL
- Build your project in Debug mode:
-
Place Symbols in KamiYomu
- Copy the
.pdbfile to KamiYomu's agent folder:C:\Users\[YourUsername]\AppData\Local\KamiYomu\agents\[DeveloperName].ActionAgents.[ProductName]\lib\net8.0\
- Copy the
-
Enable Debugging in KamiYomu.Web
- Attach to the running KamiYomu process using Visual Studio
- Set breakpoints in your action code
- KamiYomu will pause execution at your breakpoints with full source visibility
public async Task ExecuteAsync(
ActionAgentContext context,
IDictionary<string, object> options,
CancellationToken cancellationToken)
{
try
{
Logger?.LogInformation("Starting action execution");
Logger?.LogDebug($"Manga: {context.Manga?.Title}");
// Your action logic here
Logger?.LogInformation("Action completed successfully");
}
catch (Exception ex)
{
Logger?.LogError(ex, "Action execution failed");
throw;
}
}public class NotificationActionAgent : AbstractActionAgent, IActionAgent
{
public async Task ExecuteAsync(
ActionAgentContext context,
IDictionary<string, object> options,
CancellationToken cancellationToken)
{
var message = $"Chapter {context.Chapter?.Number} of {context.Manga?.Title} " +
$"was downloaded at {context.TriggerContext?.TriggeredAtUtc:G}";
// Send notification (e.g., via webhook, email, Discord, etc.)
await SendNotificationAsync(message, cancellationToken);
}
private async Task SendNotificationAsync(string message, CancellationToken cancellationToken)
{
// Implementation here
await Task.CompletedTask;
}
}public class WebhookActionAgent : AbstractActionAgent, IActionAgent
{
private readonly HttpClient _httpClient;
public WebhookActionAgent(IDictionary<string, object> options) : base(options)
{
_httpClient = new HttpClient();
}
public async Task ExecuteAsync(
ActionAgentContext context,
IDictionary<string, object> options,
CancellationToken cancellationToken)
{
var payload = new
{
manga = context.Manga?.Title,
chapter = context.Chapter?.Number,
trigger = context.TriggerContext?.Source,
timestamp = context.TriggerContext?.TriggeredAtUtc
};
var content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8,
"application/json");
try
{
var response = await _httpClient.PostAsync(
"https://your-webhook-url.com/manga-action",
content,
cancellationToken);
if (response.IsSuccessStatusCode)
{
Logger?.LogInformation("Webhook sent successfully");
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Failed to send webhook");
throw;
}
}
}public class ConditionalActionAgent : AbstractActionAgent, IActionAgent
{
public async Task ExecuteAsync(
ActionAgentContext context,
IDictionary<string, object> options,
CancellationToken cancellationToken)
{
var triggerSource = context.TriggerContext?.Source;
switch (triggerSource)
{
case ActionTriggerSource.Manual:
await HandleManualTriggerAsync(context, cancellationToken);
break;
case ActionTriggerSource.ChapterDownloader:
await HandleChapterDownloadAsync(context, cancellationToken);
break;
case ActionTriggerSource.MangaDownloader:
await HandleMangaDownloadAsync(context, cancellationToken);
break;
default:
Logger?.LogWarning($"Unhandled trigger source: {triggerSource}");
break;
}
}
private async Task HandleManualTriggerAsync(ActionAgentContext context, CancellationToken ct)
{
Logger?.LogInformation("User manually triggered this action");
await Task.CompletedTask;
}
private async Task HandleChapterDownloadAsync(ActionAgentContext context, CancellationToken ct)
{
Logger?.LogInformation($"Chapter downloaded: {context.Chapter?.Number}");
await Task.CompletedTask;
}
private async Task HandleMangaDownloadAsync(ActionAgentContext context, CancellationToken ct)
{
Logger?.LogInformation($"Manga series downloaded: {context.Manga?.Title}");
await Task.CompletedTask;
}
}# Create new action agent project
dotnet new classlib -n [DeveloperName].ActionAgents.[ProductName] -f net8.0
cd [DeveloperName].ActionAgents.[ProductName]
# Add the core library
dotnet add package KamiYomu.ActionAgents.Core
# Create test project
cd ..
dotnet new xunit -n [DeveloperName].ActionAgents.[ProductName].Tests -f net8.0
cd [DeveloperName].ActionAgents.[ProductName].Tests
dotnet add package KamiYomu.ActionAgents.Core
dotnet add package Moq
dotnet add reference ../[DeveloperName].ActionAgents.[ProductName]/YourName.ActionAgents.MyAction.csproj# Build the project
dotnet build
# Run unit tests
dotnet test
# Build release package
dotnet pack -c Release
# View package contents
dotnet nuget locals all --list# Publish to NuGet.org
dotnet nuget push bin/Release/YourName.ActionAgents.MyAction.*.nupkg \
--api-key YOUR_API_KEY \
--source https://api.nuget.org/v3/index.json
# Publish to GitHub Packages
dotnet nuget push bin/Release/YourName.ActionAgents.MyAction.*.nupkg \
--api-key YOUR_GITHUB_TOKEN \
--source https://nuget.pkg.github.com/YourOrganization/index.jsonChecklist:
- ✅ Package name follows the pattern:
*.ActionAgents.* - ✅
PackageTagsin.csprojinclude bothkamiyomuandkamiyomu-action-agents - ✅ Your class implements
IActionAgentinterface - ✅ The class has a public constructor accepting
IDictionary<string, object> options - ✅ Package is installed in KamiYomu's agent folder
- ✅ All dependencies are included in the package
Debug Steps:
# Verify package contents
unzip -l bin/Release/YourName.ActionAgents.MyAction.*.nupkg
# Check that your class is public and accessible
dotnet build --configuration Release --verbosity diagnostic- ✅ Ensure all NuGet dependencies are listed in your
.csproj - ✅ Verify .NET 8.0 target framework matches KamiYomu's runtime
- ✅ Check that custom types are public and properly namespaced
- ✅ Verify you're calling
Logger?.LogInformation()(with null-coalescing) - ✅ Ensure logger is passed in options with key
"KamiYomuILogger" - ✅ Check KamiYomu's logging configuration level (may filter out Debug messages)
- ✅ Ensure test project also targets
net8.0 - ✅ Mock dependencies properly (e.g.,
ILogger, HTTP clients) - ✅ Use the provided
ActionAgentContextBuilderfor building test contexts - ✅ Handle
CancellationTokenproperly in async tests
- ✅ Implement proper async/await patterns
- ✅ Avoid blocking operations (use
async Taskinstead ofTask.Run()) - ✅ Implement cancellation support via
CancellationToken - ✅ Set reasonable timeout values for external calls
// Good: Proper async pattern
public async Task ExecuteAsync(
ActionAgentContext context,
IDictionary<string, object> options,
CancellationToken cancellationToken)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
await SomeLongOperationAsync(cts.Token);
}
catch (OperationCanceledException)
{
Logger?.LogWarning("Action execution was cancelled");
}
}
// Avoid: Blocking patterns
// ❌ Task.Run(() => { /* blocking code */ }).Wait();
// ❌ Task.Delay(1000).Wait();| Property | Type | Description |
|---|---|---|
Manga |
MangaContext |
Information about the manga being processed |
Chapter |
ChapterContext |
Information about the chapter being processed |
TriggerContext |
ActionTriggerContext |
Information about what triggered this action |
| Property | Type | Description |
|---|---|---|
Title |
string |
The manga series title |
URL |
string |
The source URL of the manga |
Description |
string |
Manga description or synopsis |
| Property | Type | Description |
|---|---|---|
Number |
string |
Chapter number or identifier |
URL |
string |
The URL to the chapter |
ReleaseDateUtc |
DateTime? |
When the chapter was released (UTC) |
| Property | Type | Description |
|---|---|---|
Source |
ActionTriggerSource |
What triggered this action (see table above) |
TriggeredAtUtc |
DateTime |
When the action was triggered (UTC) |
- Core Library Repository: https://github.com/KamiYomu/KamiYomu.ActionAgents.Core
- Main KamiYomu Project: https://github.com/KamiYomu/KamiYomu
Join the conversation and be part of the KamiYomu community:
| Action | Link |
|---|---|
| Following | |
| Discord | |
| Sponsor | |
| Report | |
| Contribute |
- Follow SOLID principles in your action design
- Keep actions focused and single-responsibility
- Document all configuration options
- Add comprehensive error logging
- Write unit tests for your actions
- Use semantic versioning for your package
This project is licensed under the MIT License for the library code.
See the LICENSE file in the repository for full terms.
© KamiYomu. Licensed under AGPL-3.0 for the KamiYomu project itself.
The KamiYomu.ActionAgents.Core library is provided under the MIT License to enable community contributions and third-party action development.