Skip to content

Long Task Completion Notifications

“Left for lunch while the model is training” — a classic. To avoid coming back in an hour only to find the job finished 50 minutes ago (or crashed after 30 seconds), just send a message to Notifly from the script itself or its wrapper.

Run any long command through this wrapper — you’ll get a message about success/failure and duration:

/usr/local/bin/notifly-run
#!/usr/bin/env bash
set -eu
set -a; source ~/.notifly.env; set +a
CMD="$*"
START=$(date +%s)
HOST=$(hostname -s)
LOG=$(mktemp)
if "$@" 2>&1 | tee "$LOG"; then
DUR=$(( $(date +%s) - START ))
LAST=$(tail -c 800 "$LOG")
/usr/local/bin/notifly-send \
"✅ ${CMD:0:60}" \
"Длительность: ${DUR}s
Хост: $HOST
Конец вывода:
$LAST" 5
else
RC=$?
DUR=$(( $(date +%s) - START ))
LAST=$(tail -c 800 "$LOG")
/usr/local/bin/notifly-send \
"❌ ${CMD:0:60} (rc=$RC)" \
"Длительность: ${DUR}s
Конец вывода:
$LAST" 9
fi
rm -f "$LOG"

Usage:

Окно терминала
notifly-run python train.py --epochs 100
notifly-run rsync -aHAX /huge /backup/
notifly-run docker compose pull

Windows equivalent of notifly-run. Use the Send-Notifly function from the reminders recipe.

C:\scripts\Notifly-Run.ps1
param([Parameter(Mandatory, ValueFromRemainingArguments)] [string[]]$Cmd)
. $env:USERPROFILE\Documents\WindowsPowerShell\Notifly.ps1
$start = Get-Date
$log = New-TemporaryFile
$label = ($Cmd -join ' ').Substring(0, [Math]::Min(60, ($Cmd -join ' ').Length))
& $Cmd[0] $Cmd[1..($Cmd.Count-1)] *> $log
$rc = $LASTEXITCODE
$dur = ((Get-Date) - $start).TotalSeconds
$tail = (Get-Content $log -Tail 20) -join "`n"
if ($rc -eq 0) {
Send-Notifly -Title "$label" `
-Message "Длительность: $([int]$dur)s`nХост: $env:COMPUTERNAME`n`nКонец вывода:`n$tail" `
-Priority 5
} else {
Send-Notifly -Title "$label (rc=$rc)" `
-Message "Длительность: $([int]$dur)s`n`nКонец вывода:`n$tail" `
-Priority 9
}
Remove-Item $log -Force
exit $rc
Окно терминала
# Alias in $PROFILE
Set-Alias nrun C:\scripts\Notifly-Run.ps1
nrun python train.py --epochs 100
nrun docker compose pull
nrun robocopy C:\src D:\backup /MIR

The Python examples on this page (decorator, Jupyter magic) work on Windows without changes — install httpx and set the environment variables:

Окно терминала
[Environment]::SetEnvironmentVariable("NOTIFLY_URL", "https://your-notifly.example.com", "User")
[Environment]::SetEnvironmentVariable("NOTIFLY_TOKEN", "AGdjfk_L.dKe8q", "User")

You can embed a notification directly into code:

import os, time, traceback, functools, httpx
def notify_on_finish(name=None, priority=5):
def deco(fn):
@functools.wraps(fn)
def wrap(*args, **kwargs):
label = name or fn.__name__
t0 = time.time()
try:
result = fn(*args, **kwargs)
_send(f"✅ {label} OK", f"Длительность: {time.time()-t0:.1f}s", priority)
return result
except Exception as e:
_send(
f"❌ {label} FAIL",
f"{type(e).__name__}: {e}\n\n{traceback.format_exc()[-1200:]}",
max(priority, 9),
)
raise
return wrap
return deco
def _send(title, message, priority):
try:
httpx.post(
f"{os.environ['NOTIFLY_URL']}/message",
params={"token": os.environ["NOTIFLY_TOKEN"]},
json={"title": title, "message": message, "priority": priority},
timeout=5,
)
except Exception:
pass
@notify_on_finish("Обучение CIFAR-10")
def train():
for epoch in range(100):
...
train()

Sometimes it’s convenient to notify after a long cell. Put this at the start of the notebook:

def notify(title, message="", priority=5):
import os, httpx
httpx.post(
f"{os.environ['NOTIFLY_URL']}/message",
params={"token": os.environ["NOTIFLY_TOKEN"]},
json={"title": title, "message": message, "priority": priority},
timeout=5,
)

And in any cell after a long run:

model.fit(train_ds, epochs=50, validation_data=val_ds)
notify("✅ fit() готов", f"val_acc={history.history['val_acc'][-1]:.3f}")

ML / experiments: key metrics in the message

Section titled “ML / experiments: key metrics in the message”

Send a brief summary — it’s convenient to read on a phone:

notify(
title=f"✅ run {run_id} done",
message=(
f"Model: {model_name}\n"
f"Time: {minutes:.1f} min\n"
f"Loss: {final_loss:.4f}\n"
f"Acc: {final_acc:.4f}\n"
f"Best: {best_acc:.4f} @ epoch {best_epoch}"
),
priority=4,
)

For very long jobs it’s useful to send “pings” to show the job hasn’t stalled:

for epoch in range(100):
train_one_epoch()
if epoch % 10 == 0 and epoch > 0:
notify(f"⏳ {epoch}/100 эпох",
f"loss={current_loss:.4f}, eta {eta_min}m",
priority=2)
  • Time freedom. You can leave and do other things instead of watching the terminal.
  • Context in the message. No need to log back into the server — the key metrics are already in your pocket.
  • No missed failures. Script crashed → you immediately see .
  • Include a link to the “full log” in the message (for example, to a gist/paste in a private S3).
  • Attach a plot (as a link to an image) via the extras["client::display"].