Posted on 18/02/2026 14:56:16
Hi Jelle
There is not simple way - but you can do a custom schedule task add in.
This BaseScheduledTaskAddIn runs as a Dynamicweb scheduled task, reads the latest row from your dbo.dw_ProductNumberJobLog, and if that row’s Status is Failed it sends a templated email (uses {JobName}, {LogId}, {Status}, {RunDate}, {Message} placeholders). It avoids duplicate alerts by storing the last notified LogId in a small helper table (dbo.dw_ScheduledTaskNotifications). Sending uses Dynamicweb’s EmailHandler (falls back to system notification emails / EmailHandler.SystemMailFrom() if ToEmail/FromEmail aren’t provided). All behaviour (From, To, Template, optional DB/SMTP overrides) is configurable through the scheduled-task parameters, and any email send error causes the task to surface a failure so you can see it in DW.
This is made using AI and completely untestet. Use it as a starting point.
using Dynamicweb.Data;
using Dynamicweb.Extensibility.AddIns;
using Dynamicweb.Extensibility.Editors;
using Dynamicweb.Logging;
using Dynamicweb.Configuration;
using Dynamicweb.Mailing;
using System;
using System.Data.SqlClient;
using System.Linq;
using System.Net.Mail;
using System.Text;
namespace Dynamicweb.Scheduling.ScheduledTaskAddIns
{
[AddInName(".ProductNumberJobNotificationAddIn"), AddInLabel("ProductNumberJob Notification"), AddInDescription("Sends an e-mail when the latest row in dbo.dw_ProductNumberJobLog is Failed.")]
public class ProductNumberJobNotificationAddIn : BaseScheduledTaskAddIn
{
// Minimal settings (required)
[AddInParameter("From email")]
public string? FromEmail { get; set; }
[AddInParameter("To email (comma separated)"), AddInParameterEditor(typeof(TextParameterEditor), "inputClass=NewUIinput;TextArea=False;")]
public string? ToEmail { get; set; }
[AddInParameter("Template (use placeholders: {JobName}, {LogId}, {Status}, {RunDate}, {Message})"), AddInParameterEditor(typeof(TextParameterEditor), "inputClass=NewUIinput;TextArea=True;style=height:120px;")]
public string? Template { get; set; }
// Optional: explicit connection string (otherwise use site default)
[AddInParameter("ConnectionString (optional)")]
public string? ConnectionString { get; set; }
// Optional SMTP override (kept for backwards compatibility, but not used when EmailHandler.Send is available)
[AddInParameter("SmtpHost (optional)")]
public string? SmtpHost { get; set; }
[AddInParameter("SmtpPort (optional)")]
public string? SmtpPort { get; set; }
[AddInParameter("SmtpUseSsl (optional)")]
public string? SmtpUseSsl { get; set; }
[AddInParameter("SmtpUser (optional)")]
public string? SmtpUser { get; set; }
[AddInParameter("SmtpPass (optional)")]
public string? SmtpPass { get; set; }
public override bool Run()
{
ILogger logger = LogManager.Current.GetLogger("ScheduledTasks", Task?.LogFileName ?? "");
try
{
logger.Log($"Task {ScheduledTaskName} started.");
// Validate required parameters (ToEmail optional — fallback to system notification emails)
if (string.IsNullOrWhiteSpace(FromEmail))
{
// It's OK if FromEmail is empty; we'll fall back to EmailHandler.SystemMailFrom()
}
var connString = GetEffectiveConnectionString();
if (string.IsNullOrWhiteSpace(connString))
{
logger.Error("No database connection string could be found. Set the 'ConnectionString' scheduled task parameter or configure site connection string.");
return false;
}
using (var conn = new SqlConnection(connString))
{
conn.Open();
// Get latest job log
var latestCmd = new SqlCommand(@"
SELECT TOP 1 LogId, Status, RunDate, Message
FROM dbo.dw_ProductNumberJobLog
ORDER BY RunDate DESC, LogId DESC", conn);
int? latestLogId = null;
string latestStatus = null;
DateTime? latestRunDate = null;
string latestMessage = null;
using (var r = latestCmd.ExecuteReader())
{
if (r.Read())
{
if (!r.IsDBNull(0)) latestLogId = r.GetInt32(0);
if (!r.IsDBNull(1)) latestStatus = r.GetString(1);
if (!r.IsDBNull(2)) latestRunDate = r.GetDateTime(2);
if (r.FieldCount >= 4 && !r.IsDBNull(3)) latestMessage = r.GetString(3);
}
else
{
logger.Log("No rows found in dbo.dw_ProductNumberJobLog; nothing to do.");
return true;
}
}
if (latestStatus == null)
{
logger.Log("Latest row has no Status; nothing to do.");
return true;
}
if (!string.Equals(latestStatus, "Failed", StringComparison.OrdinalIgnoreCase))
{
logger.Log($"Latest status is '{latestStatus}' — only 'Failed' triggers a notification. Exiting.");
return true;
}
if (!latestLogId.HasValue)
{
logger.Log("Latest failed row has no LogId — cannot track notifications without LogId. Exiting.");
return true;
}
// Check last notified
var jobName = GetParameterValue("JobName") ?? ScheduledTaskName ?? "ProductNumberJob";
var selectLastNotified = new SqlCommand("SELECT LastNotifiedLogId FROM dbo.dw_ScheduledTaskNotifications WHERE JobName = @JobName", conn);
selectLastNotified.Parameters.AddWithValue("@JobName", jobName);
int? lastNotified = null;
var obj = selectLastNotified.ExecuteScalar();
if (obj != null && obj != DBNull.Value)
{
if (int.TryParse(obj.ToString(), out var tmp)) lastNotified = tmp;
}
if (lastNotified.HasValue && lastNotified.Value == latestLogId.Value)
{
logger.Log($"Already notified for LogId {latestLogId.Value}. Exiting.");
return true;
}
// Render template
var template = Template ?? "Scheduled task {JobName} failed. LogId: {LogId}, Status: {Status}, RunDate: {RunDate}";
var body = RenderTemplate(template, jobName, latestLogId.Value, latestStatus, latestRunDate, latestMessage);
var subject = $"Scheduled task '{jobName}' failed (LogId {latestLogId.Value})";
// Send mail using Dynamicweb EmailHandler (preferred)
SendMail(FromEmail, ToEmail, subject, body, logger);
// Upsert last notified
var existsCmd = new SqlCommand("SELECT COUNT(1) FROM dbo.dw_ScheduledTaskNotifications WHERE JobName = @JobName", conn);
existsCmd.Parameters.AddWithValue("@JobName", jobName);
var exists = (int)existsCmd.ExecuteScalar();
if (exists > 0)
{
var upd = new SqlCommand("UPDATE dbo.dw_ScheduledTaskNotifications SET LastNotifiedLogId = @LogId, NotifiedAt = GETUTCDATE() WHERE JobName = @JobName", conn);
upd.Parameters.AddWithValue("@LogId", latestLogId.Value);
upd.Parameters.AddWithValue("@JobName", jobName);
upd.ExecuteNonQuery();
}
else
{
var ins = new SqlCommand("INSERT INTO dbo.dw_ScheduledTaskNotifications (JobName, LastNotifiedLogId, NotifiedAt) VALUES (@JobName, @LogId, GETUTCDATE())", conn);
ins.Parameters.AddWithValue("@JobName", jobName);
ins.Parameters.AddWithValue("@LogId", latestLogId.Value);
ins.ExecuteNonQuery();
}
logger.Log($"Notification processed for LogId {latestLogId.Value}.");
}
logger.Log($"Task {ScheduledTaskName} finished.");
return true;
}
catch (Exception ex)
{
LogManager.System.GetLogger(LogCategory.Provider, GetType().FullName ?? "").Error("ProductNumberJobNotificationAddIn error", ex);
return false;
}
}
private string RenderTemplate(string template, string jobName, int logId, string status, DateTime? runDate, string? message)
{
var runDateText = runDate.HasValue ? runDate.Value.ToString("u") : "";
return template
.Replace("{JobName}", jobName)
.Replace("{LogId}", logId.ToString())
.Replace("{Status}", status ?? "")
.Replace("{RunDate}", runDateText)
.Replace("{Message}", message ?? "");
}
private void SendMail(string fromEmail, string? toEmails, string subject, string body, ILogger logger)
{
try
{
using var mail = new MailMessage();
// Determine recipients: use ToEmail param if set, otherwise fall back to system notification emails
if (string.IsNullOrWhiteSpace(toEmails))
{
var systemEmails = SystemConfiguration.Instance.GetNotificationEmails();
if (systemEmails == null || systemEmails.Length == 0)
{
logger.Error("No recipient configured (ToEmail empty and system notification emails are empty). Aborting send.");
throw new InvalidOperationException("No recipient configured for ProductNumberJobNotificationAddIn.");
}
foreach (var em in systemEmails)
{
if (!string.IsNullOrWhiteSpace(em))
mail.To.Add(em);
}
}
else
{
var tos = toEmails.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim());
foreach (var to in tos)
{
mail.To.Add(new MailAddress(to));
}
}
// Determine sender: prefer explicit FromEmail, otherwise system mail
if (!string.IsNullOrWhiteSpace(fromEmail))
{
var fromAddr = new MailAddress(fromEmail);
mail.Sender = fromAddr;
mail.From = fromAddr;
}
else
{
var systemFrom = EmailHandler.SystemMailFrom();
if (systemFrom != null)
{
mail.Sender = systemFrom;
mail.From = systemFrom;
}
else
{
logger.Error("No FromEmail provided and EmailHandler.SystemMailFrom() returned null.");
throw new InvalidOperationException("No sender address configured.");
}
}
mail.Subject = subject;
mail.Body = body;
mail.IsBodyHtml = true;
mail.BodyEncoding = Encoding.UTF8;
// Use EmailHandler.Send (your example pattern)
if (!EmailHandler.Send(mail, true, false))
{
logger.Debug("Failed to send ProductNumberJob notification using EmailHandler.Send.");
throw new InvalidOperationException("EmailHandler.Send returned false.");
}
logger.Log($"Email sent (subject: {subject}) to {string.Join(", ", mail.To.Cast<MailAddress>().Select(a => a.Address))}.");
}
catch (Exception ex)
{
logger.Error("Failed to send notification email.", ex);
// Re-throw so DW scheduler marks the task as failed (adjust if you prefer to swallow send errors)
throw;
}
}
private string? GetParameterValue(string key)
{
try
{
var prop = GetType().GetProperty(key);
if (prop != null)
{
var val = prop.GetValue(this) as string;
if (!string.IsNullOrWhiteSpace(val)) return val;
}
if (Task?.Parameters is System.Collections.IDictionary dict && dict.Contains(key))
{
return dict[key]?.ToString();
}
if (Task?.Parameters is System.Collections.Generic.IDictionary<string, string> dict2 && dict2.TryGetValue(key, out var v))
{
return v;
}
}
catch { /* swallow */ }
return null;
}
private string GetEffectiveConnectionString()
{
if (!string.IsNullOrWhiteSpace(ConnectionString))
return ConnectionString;
var param = GetParameterValue("ConnectionString");
if (!string.IsNullOrWhiteSpace(param))
return param;
try
{
var cs = Database.ConnectionString;
if (!string.IsNullOrEmpty(cs)) return cs;
}
catch { /* ignore */ }
try
{
var csFromConfig = System.Configuration.ConfigurationManager.ConnectionStrings["Dynamicweb"]?.ConnectionString;
if (!string.IsNullOrEmpty(csFromConfig)) return csFromConfig;
}
catch { /* ignore */ }
var env = Environment.GetEnvironmentVariable("DW_CONNECTIONSTRING");
if (!string.IsNullOrEmpty(env)) return env;
throw new InvalidOperationException("No connection string found.");
}
}
}
And the table needed to hold history:
CREATE TABLE dbo.dw_ScheduledTaskNotifications
(
JobName NVARCHAR(200) NOT NULL PRIMARY KEY,
LastNotifiedLogId INT NULL,
NotifiedAt DATETIME2 NULL
);