{"id":1304,"date":"2026-02-15T20:14:30","date_gmt":"2026-02-15T20:14:30","guid":{"rendered":"https:\/\/docs.ai.drawconclusions.org\/?page_id=1304"},"modified":"2026-02-24T12:45:55","modified_gmt":"2026-02-24T12:45:55","slug":"cronjobs","status":"publish","type":"page","link":"https:\/\/docs.ai.drawconclusions.org\/?page_id=1304","title":{"rendered":"CronJobs"},"content":{"rendered":"<h2>Overview<\/h2>\n<ul>\n<li>CronJobs are defined in the <code>cron_jobs<\/code> table.<\/li>\n<li>The app runs a check once per minute and executes due jobs via CronJob service subclasses.<\/li>\n<li>Runner endpoint: <code>POST \/api\/cron\/run<\/code>.<\/li>\n<\/ul>\n<h2>Conceptual<\/h2>\n<h2>Concept<\/h2>\n<ul>\n<li>The app runs scheduled jobs defined in the database.<\/li>\n<li>Only active jobs that are due are executed.<\/li>\n<li>Jobs are removed from the <code>cron_jobs<\/code> table after a successful run.<\/li>\n<\/ul>\n<h2>Table<\/h2>\n<ul>\n<li><code>cron_jobs.name<\/code> identifies the job and maps to a CronJob subclass.<\/li>\n<li><code>cron_jobs.job_type<\/code> maps to the CronJob subclass.<\/li>\n<li><code>cron_jobs.user_id<\/code> links a job to a specific user (when applicable).<\/li>\n<li><code>cron_jobs.run_at<\/code> defines one-time schedules.<\/li>\n<li><code>cron_jobs.interval_minutes<\/code> defines recurring schedules.<\/li>\n<li><code>cron_jobs.is_active<\/code> enables\/disables a job.<\/li>\n<li><code>cron_jobs.last_run_at<\/code> tracks the last execution time.<\/li>\n<li>After a job runs successfully, its record is deleted from <code>cron_jobs<\/code>.<\/li>\n<\/ul>\n<h2>Implementation<\/h2>\n<ul>\n<li>BE cron runner: <code>server\/var_www\/symfony_be\/public\/index.php<\/code> (<code>\/api\/cron\/run<\/code>)<\/li>\n<li>CronJob runner service: <code>server\/var_www\/symfony_be\/src\/Service\/CronJob\/CronJobRunner.php<\/code><\/li>\n<li>CronJob interface: <code>server\/var_www\/symfony_be\/src\/Service\/CronJob\/CronJobInterface.php<\/code><\/li>\n<li>Example job: <code>server\/var_www\/symfony_be\/src\/Service\/CronJob\/NoopCronJob.php<\/code><\/li>\n<li>Reset token invalidation job: <code>server\/var_www\/symfony_be\/src\/Service\/CronJob\/InvalidateResetPasswordCode.php<\/code><\/li>\n<li>Pending registration invalidation job: <code>server\/var_www\/symfony_be\/src\/Service\/CronJob\/InvalidatePendingRegistration.php<\/code><\/li>\n<li>Server cron runner script: <code>server\/var_www\/scripts_server\/cron_runner.py<\/code> (calls <code>\/api\/cron\/run<\/code>)<\/li>\n<li>Tests status cronjob script: <code>server\/var_www\/scripts_server\/tests_cronjob_runner.py<\/code> (writes <code>freshness\/all_tests_cronjob_fresh.html<\/code>).<\/li>\n<\/ul>\n<h2>Tables<\/h2>\n<ul>\n<li><code>table_cron_jobs.md<\/code> (cron_jobs, schema details live there)<\/li>\n<li><code>table_users.md<\/code> (users, relation via <code>user_id<\/code>)<\/li>\n<\/ul>\n<h2>Services<\/h2>\n<ul>\n<li><code>service_be_php.md<\/code><\/li>\n<li><code>service_db.md<\/code><\/li>\n<\/ul>\n<h2>Routes<\/h2>\n<ul>\n<li><code>route_api_cron_run.md<\/code><\/li>\n<\/ul>\n<h2>Schedule<\/h2>\n<ul>\n<li>Cron job (server): <code>* * * * * \/usr\/bin\/python3 \/var\/www\/scripts_server\/cron_runner.py<\/code><\/li>\n<li>Tests status cronjob (server): <code>* * * * * \/usr\/bin\/python3 \/var\/www\/scripts_server\/tests_cronjob_runner.py<\/code><\/li>\n<li>Freshness watchdog (server): <code>* * * * * \/usr\/bin\/python3 \/var\/www\/scripts_server\/notify_stale_jobs.py<\/code><\/li>\n<\/ul>\n<p class=\"developerdocs-id-search\" aria-hidden=\"true\">DeveloperDoc ID: dd_id_d22f855f28<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Overview CronJobs are defined in the cron_jobs table. The app runs a check once per minute and executes due jobs via CronJob service subclasses. Runner endpoint: POST \/api\/cron\/run. Conceptual Concept The app runs scheduled jobs defined in the database. Only active jobs that are due are executed. Jobs are removed from the cron_jobs table after [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":1289,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"doc_category":[],"doc_layer":[30],"class_list":["post-1304","page","type-page","status-publish","hentry","layer-dd_id_8f59177ca0"],"_links":{"self":[{"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=\/wp\/v2\/pages\/1304","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1304"}],"version-history":[{"count":5,"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=\/wp\/v2\/pages\/1304\/revisions"}],"predecessor-version":[{"id":3820,"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=\/wp\/v2\/pages\/1304\/revisions\/3820"}],"up":[{"embeddable":true,"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=\/wp\/v2\/pages\/1289"}],"wp:attachment":[{"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1304"}],"wp:term":[{"taxonomy":"doc_category","embeddable":true,"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=%2Fwp%2Fv2%2Fdoc_category&post=1304"},{"taxonomy":"doc_layer","embeddable":true,"href":"https:\/\/docs.ai.drawconclusions.org\/index.php?rest_route=%2Fwp%2Fv2%2Fdoc_layer&post=1304"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}