diff --git a/openedx/core/djangoapps/programs/admin.py b/openedx/core/djangoapps/programs/admin.py
index 44b95b707b18e90fd9ae1c0bb7c7a5aca0c3c04e..90ba0d541dd4f5234907693e56280d8eb9107554 100644
--- a/openedx/core/djangoapps/programs/admin.py
+++ b/openedx/core/djangoapps/programs/admin.py
@@ -6,7 +6,7 @@ from __future__ import absolute_import
 from config_models.admin import ConfigurationModelAdmin
 from django.contrib import admin
 
-from openedx.core.djangoapps.programs.models import ProgramsApiConfig
+from openedx.core.djangoapps.programs.models import ProgramsApiConfig, CustomProgramsConfig
 
 
 class ProgramsApiConfigAdmin(ConfigurationModelAdmin):
@@ -14,3 +14,4 @@ class ProgramsApiConfigAdmin(ConfigurationModelAdmin):
 
 
 admin.site.register(ProgramsApiConfig, ProgramsApiConfigAdmin)
+admin.site.register(CustomProgramsConfig, ConfigurationModelAdmin)
diff --git a/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py b/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py
index 592eb5c58c6d6bad873367b13d4916276f97dbc3..dd00caa3bed790e16f42b7ec1087b5fc945987bf 100644
--- a/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py
+++ b/openedx/core/djangoapps/programs/management/commands/backpopulate_program_credentials.py
@@ -3,10 +3,9 @@ from __future__ import absolute_import
 
 import logging
 from collections import namedtuple
-from functools import reduce  # pylint: disable=redefined-builtin
-
+from functools import reduce  # pylint: disable=redefined-builtin, useless-suppression
 from django.contrib.sites.models import Site
-from django.core.management import BaseCommand
+from django.core.management.base import BaseCommand, CommandError
 from django.db.models import Q
 from opaque_keys.edx.keys import CourseKey
 
@@ -14,6 +13,8 @@ from course_modes.models import CourseMode
 from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
 from openedx.core.djangoapps.catalog.utils import get_programs
 from openedx.core.djangoapps.programs.tasks.v1.tasks import award_program_certificates
+from openedx.core.djangoapps.programs.models import CustomProgramsConfig
+
 
 # TODO: Log to console, even with debug mode disabled?
 logger = logging.getLogger(__name__)  # pylint: disable=invalid-name
@@ -38,13 +39,49 @@ class Command(BaseCommand):
             default=False,
             help='Submit tasks for processing.'
         )
+        parser.add_argument(
+            '--args-from-database',
+            action='store_true',
+            default=False,
+            help='Use arguments from the Config model instead of the command line.',
+        )
+        parser.add_argument(
+            '--program-uuids',
+            nargs='+',
+            help='Award certificates only for specific programs.',
+        )
+        parser.add_argument(
+            '--usernames',
+            nargs='+',
+            help='Award certificates only to specific users.',
+        )
+
+    def get_args_from_database(self):
+        """ Returns an options dictionary from the current NotifyCredentialsConfig model. """
+        config = CustomProgramsConfig.current()
+        if not config.enabled:
+            raise CommandError('CustomProgramsConfig is disabled, but --args-from-database was requested.')
+
+        # We don't need fancy shell-style whitespace/quote handling - none of our arguments are complicated
+        argv = config.arguments.split()
+
+        parser = self.create_parser('manage.py', 'backpopulate_program_credentials')
+        return parser.parse_args(argv).__dict__   # we want a dictionary, not a non-iterable Namespace object
 
     def handle(self, *args, **options):
+        program_uuids, usernames = None, None
+        if options['args_from_database']:
+            logger.info('Loading arguments from the database for custom programs or learners.')
+
+            arguments = self.get_args_from_batabase()  # pylint: disable=no-member
+            program_uuids = arguments.get('program-uuids', None)
+            usernames = arguments.get('usernames', None)
+
         logger.info('Loading programs from the catalog.')
-        self._load_course_runs()
+        self._load_course_runs(program_uuids=program_uuids)
 
         logger.info('Looking for users who may be eligible for a program certificate.')
-        self._load_usernames()
+        self._load_usernames(users=usernames)
 
         if options.get('commit'):
             logger.info(u'Enqueuing program certification tasks for %d candidates.', len(self.usernames))
@@ -73,12 +110,15 @@ class Command(BaseCommand):
             failed
         )
 
-    def _load_course_runs(self):
+    def _load_course_runs(self, program_uuids=None):
         """Find all course runs which are part of a program."""
         programs = []
-        for site in Site.objects.all():
-            logger.info(u'Loading programs from the catalog for site %s.', site.domain)
-            programs.extend(get_programs(site))
+        if program_uuids:
+            programs.extend(get_programs(uuids=program_uuids))
+        else:
+            for site in Site.objects.all():
+                logger.info(u'Loading programs from the catalog for site %s.', site.domain)
+                programs.extend(get_programs(site))
 
         self.course_runs = self._flatten(programs)
 
@@ -95,7 +135,7 @@ class Command(BaseCommand):
 
         return course_runs
 
-    def _load_usernames(self):
+    def _load_usernames(self, users=None):
         """Identify a subset of users who may be eligible for a program certificate.
 
         This is done by finding users who have earned a qualifying certificate in
@@ -118,3 +158,6 @@ class Command(BaseCommand):
             query
         ).values('user__username').distinct()
         self.usernames = [d['user__username'] for d in username_dicts]
+        if users:
+            # keeping only those learners who are in the arguments
+            self.usernames = list(set(self.usernames) & set(users))
diff --git a/openedx/core/djangoapps/programs/migrations/0013_customprogramsconfig.py b/openedx/core/djangoapps/programs/migrations/0013_customprogramsconfig.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e2260001e1080d0b9acc804aeb4c4a5a60f57b1
--- /dev/null
+++ b/openedx/core/djangoapps/programs/migrations/0013_customprogramsconfig.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.26 on 2019-12-13 07:44
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('programs', '0012_auto_20170419_0018'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='CustomProgramsConfig',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
+                ('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
+                ('arguments', models.TextField(blank=True, default='', help_text='Useful for manually running a Jenkins job. Specify like "--usernames A B --program-uuids X Y".')),
+                ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
+            ],
+            options={
+                'verbose_name': 'backpopulate_program_credentials argument',
+            },
+        ),
+    ]
diff --git a/openedx/core/djangoapps/programs/models.py b/openedx/core/djangoapps/programs/models.py
index 0cf5c0bb198b40d8bae35abb0ebc24d16c328d6d..1ac3fb749c455d64f9eef14802946e4660fc837c 100644
--- a/openedx/core/djangoapps/programs/models.py
+++ b/openedx/core/djangoapps/programs/models.py
@@ -2,6 +2,7 @@
 
 from __future__ import absolute_import
 
+import six
 from config_models.models import ConfigurationModel
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
@@ -25,3 +26,21 @@ class ProgramsApiConfig(ConfigurationModel):
             'Path used to construct URLs to programs marketing pages (e.g., "/foo").'
         )
     )
+
+
+class CustomProgramsConfig(ConfigurationModel):  # pylint: disable=model-missing-unicode, useless-suppression
+    """
+    Manages configuration for a run of the backpopulate_program_credentials management command.
+    """
+    class Meta(object):
+        app_label = 'programs'
+        verbose_name = 'backpopulate_program_credentials argument'
+
+    arguments = models.TextField(
+        blank=True,
+        help_text='Useful for manually running a Jenkins job. Specify like "--usernames A B --program-uuids X Y".',
+        default='',
+    )
+
+    def __str__(self):
+        return six.text_type(self.arguments)
diff --git a/openedx/core/djangoapps/programs/tasks/v1/tasks.py b/openedx/core/djangoapps/programs/tasks/v1/tasks.py
index 457b24559391ecd932acf145316300a14c394dc6..24454febbdc74b3a934c8b620d5a409ae9bc4abd 100644
--- a/openedx/core/djangoapps/programs/tasks/v1/tasks.py
+++ b/openedx/core/djangoapps/programs/tasks/v1/tasks.py
@@ -193,6 +193,8 @@ def award_program_certificates(self, username):
         for program_uuid in new_program_uuids:
             visible_date = completed_programs[program_uuid]
             try:
+                LOGGER.info(u'Visible date for user %s : program %s is %s', username, program_uuid,
+                            visible_date)
                 award_program_certificate(credentials_client, username, program_uuid, visible_date)
                 LOGGER.info(u'Awarded certificate for program %s to user %s', program_uuid, username)
             except exceptions.HttpNotFoundError: