diff --git a/edit.py b/edit.py
index e12116f1c02d39c9c67d8d03c962eebf17c7e170..f9eafb7ea944f7d0dcb5e0a34578b5559661c70f 100644
--- a/edit.py
+++ b/edit.py
@@ -119,6 +119,18 @@ def getfielddescription(path):
 		desc = '<br>'+desc
 	return desc
 
+edit_handlers = {}
+def edit_handler(*tables, field=None):
+	def wrapper(func):
+		for table in tables:
+			if table not in edit_handlers:
+				edit_handlers[table] = {}
+			if field not in edit_handlers[table]:
+				edit_handlers[table][field] = []
+			edit_handlers[table][field].append(func)
+		return func
+	return wrapper
+
 @app.route('/internal/edit', methods=['GET', 'POST'])
 @mod_required
 @csrf_protect
@@ -141,6 +153,10 @@ def edit(prefix='', ignore=[]):
 			VALUES (?,?,?,?,?,(SELECT `%s` FROM %s WHERE %s = ?),?,?,1)'%(path['column'], path['tableinfo']['table'], path['tableinfo']['idcolumn']),
 				path['table'], path['id'], path['tableinfo']['idcolumn'], path['column'], val, path['id'], datetime.now(), session['user']['dbid'])
 		modify('UPDATE %s SET `%s` = ?, time_updated = ? WHERE `%s` = ?'%(path['tableinfo']['table'], path['column'], path['tableinfo']['idcolumn']), val, datetime.now(),path['id'])
+		for func in edit_handlers.get(path['table'], {}).get(None, []):
+			func(path['table'], path['column'], val, path['id'])
+		for func in edit_handlers.get(path['table'], {}).get(path['column'], []):
+			func(path['table'], path['column'], val, path['id'])
 	if 'ref' in request.values:
 		return redirect(request.values['ref'])
 	return "OK", 200
diff --git a/jobs.py b/jobs.py
index 432935beed319e7c99558bd410d49f3b4c4764f4..0718d7d5f6cb4dd87557f813d29a3a8e8dd53eb0 100644
--- a/jobs.py
+++ b/jobs.py
@@ -67,7 +67,7 @@ def jobs_api_token_required(func):
 		else:
 			token = None
 		
-		if not token == config['JOBS_API_KEY']:
+		if not token == config.get('JOBS_API_KEY', [None]):
 			return 'Permission denied', 403
 		else:
 			return func(*args, **kwargs)
@@ -99,6 +99,25 @@ def jobs_worker_ping(hostname):
 	query('REPLACE INTO worker (hostname, last_ping) values (?, ?)', hostname, datetime.now())
 	return 'OK',200
 
+job_handlers = {}
+def job_handler(*types, state='finished'):
+	def wrapper(func):
+		for jobtype in types:
+			if jobtype not in job_handlers:
+				job_handlers[jobtype] = {}
+			if jobtype not in job_handlers[jobtype]:
+				job_handlers[jobtype][state] = []
+			job_handlers[jobtype][state].append(func)
+			return func
+	return wrapper
+
+def schedule_job(jobtype, data=None, priority=0):
+	if not data:
+		data = {}
+	modify('INSERT INTO jobs (type, priority, data, time_created) VALUES (?, ?, ?, ?)',
+			jobtype, priority, json.dumps(data, default=date_json_handler), datetime.now())
+
+
 @app.route('/internal/jobs/api/job/<int:id>/ping', methods=['GET', 'POST'])
 @jobs_api_token_required
 def jobs_ping(id):
@@ -109,7 +128,13 @@ def jobs_ping(id):
 		query('UPDATE jobs SET time_finished = ?, status = ?, state = "finished" where id = ?', datetime.now(), status, id)
 	else:
 		query('UPDATE jobs SET worker = ?, last_ping = ?, status = ?, state = ? where id = ?', hostname, datetime.now(), status, state, id)
-	return 'OK',200
+	job = query('SELECT * FROM jobs WHERE id = ?', id)[0]
+	for func in job_handlers.get(job['type'], {}).get(state, []):
+		try:
+			func(id, job['type'], json.loads(job['data']), state, json.loads(job['status']))
+		except Exception:
+			traceback.print_exc()
+	return 'OK', 200
 
 @app.route('/internal/jobs/api/worker/<hostname>/schedule', methods=['POST'])
 @jobs_api_token_required
diff --git a/server.py b/server.py
index 0d6884800832851607080f4dafdd777e0a8fe1e3..e3f0acc17b3371b6040be5c8273cc9a742579b17 100644
--- a/server.py
+++ b/server.py
@@ -461,7 +461,7 @@ def dbstatus():
 		clusters[cluster].append(host)
 	return render_template('dbstatus.html', clusters=clusters, statuses=status, vars=variables), 200
 
-import edit
+from edit import edit_handler
 import feeds
 import importer
 import stats
@@ -469,8 +469,7 @@ import sorter
 if 'ICAL_URL' in config:
 	import meetings
 import l2pauth
-if 'JOBS_API_KEY' in config:
-	import jobs
+from jobs import job_handler, schedule_job
 import timetable
 import chapters
 import icalexport