diff --git a/configs/samples/extconfig.conf.sample b/configs/samples/extconfig.conf.sample
index 9e13cacda41d8e7cf5a3ed51ea2e71db9b0fc063..b633fafa61feb512c9740f29af67c6894771532d 100644
--- a/configs/samples/extconfig.conf.sample
+++ b/configs/samples/extconfig.conf.sample
@@ -95,6 +95,7 @@
 ;queue_rules => odbc,asterisk
 ;acls => odbc,asterisk
 ;musiconhold => mysql,general
+;musiconhold_entry => mysql,general
 ;queue_log => mysql,general
 ;
 ;
diff --git a/configs/samples/musiconhold.conf.sample b/configs/samples/musiconhold.conf.sample
index 741bde60379df90a9933a9f39340bbd2f56fb271..1090bbef16d1bffb5d95bfa2d11d8c8f171a6044 100644
--- a/configs/samples/musiconhold.conf.sample
+++ b/configs/samples/musiconhold.conf.sample
@@ -13,6 +13,7 @@
 ; valid mode options:
 ; files		-- read files from a directory in any Asterisk supported
 ;		   media format
+; playlist	-- provide a fixed list of filenames or URLs to play
 ; quietmp3 	-- default
 ; mp3 		-- loud
 ; mp3nb		-- unbuffered
@@ -44,6 +45,22 @@
 ; this, res_musiconhold will skip the files it is not able to
 ; understand when it loads.
 ;
+; =========
+; Playlist (native) music on hold
+; =========
+;
+; This mode is similar to 'files' mode in that it plays through a list
+; of files, but instead of scanning a directory the files are
+; explicitly configured using one or more 'entry' options.
+;
+; Each entry must be one of:
+;
+;   * An absolute path to the file to be played, without an extension.
+;   * A URL
+;
+; The entries are played in the order in which they appear in the
+; configuration. The 'sort' option is not used for this mode.
+;
 
 [default]
 mode=files
@@ -71,6 +88,12 @@ directory=moh
 ;directory=moh
 ;sort=alpha     ; Sort the files in alphabetical order.
 
+;[sales-queue-hold]
+;mode=playlist
+;entry=/var/lib/asterisk/sounds/en/yourcallisimportant
+;entry=http://example.local/sales-queue-hold-music.ulaw
+;entry=/var/lib/asterisk/moh/macroform-robot_dity
+
 ; =========
 ; Other (non-native) playback methods
 ; =========
diff --git a/contrib/ast-db-manage/config/versions/fbb7766f17bc_add_playlist_to_moh.py b/contrib/ast-db-manage/config/versions/fbb7766f17bc_add_playlist_to_moh.py
new file mode 100644
index 0000000000000000000000000000000000000000..0cb5ddb8316f860797dbdb2b7409d53058b734e5
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/fbb7766f17bc_add_playlist_to_moh.py
@@ -0,0 +1,54 @@
+"""add playlist to moh
+
+Revision ID: fbb7766f17bc
+Revises: 3a094a18e75b
+Create Date: 2019-09-18 10:24:18.731798
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'fbb7766f17bc'
+down_revision = '3a094a18e75b'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def enum_update(table_name, column_name, enum_name, enum_values):
+    if op.get_context().bind.dialect.name != 'postgresql':
+        if op.get_context().bind.dialect.name == 'mssql':
+            op.drop_constraint('ck_musiconhold_mode_moh_mode_values', 'musiconhold')
+        op.alter_column(table_name, column_name,
+                        type_=sa.Enum(*enum_values, name=enum_name))
+        return
+
+    # Postgres requires a few more steps
+    tmp = enum_name + '_tmp'
+
+    op.execute('ALTER TYPE ' + enum_name + ' RENAME TO ' + tmp)
+
+    updated = sa.Enum(*enum_values, name=enum_name)
+    updated.create(op.get_bind(), checkfirst=False)
+
+    op.execute('ALTER TABLE ' + table_name + ' ALTER COLUMN ' + column_name +
+               ' TYPE ' + enum_name + ' USING mode::text::' + enum_name)
+
+    op.execute('DROP TYPE ' + tmp)
+
+
+def upgrade():
+    op.create_table(
+        'musiconhold_entry',
+        sa.Column('name', sa.String(80), primary_key=True, nullable=False),
+        sa.Column('position', sa.Integer, primary_key=True, nullable=False),
+        sa.Column('entry', sa.String(1024), nullable=False)
+    )
+    op.create_foreign_key('fk_musiconhold_entry_name_musiconhold', 'musiconhold_entry', 'musiconhold', ['name'], ['name'])
+    enum_update('musiconhold', 'mode', 'moh_mode_values',
+                ['custom', 'files', 'mp3nb', 'quietmp3nb', 'quietmp3', 'playlist'])
+
+
+def downgrade():
+    enum_update('musiconhold', 'mode', 'moh_mode_values',
+                ['custom', 'files', 'mp3nb', 'quietmp3nb', 'quietmp3'])
+    op.drop_table('musiconhold_entry')
diff --git a/doc/CHANGES-staging/moh-playlist.txt b/doc/CHANGES-staging/moh-playlist.txt
new file mode 100644
index 0000000000000000000000000000000000000000..14cb0224accdbe2be8756ece7f0c90b0e6af10d4
--- /dev/null
+++ b/doc/CHANGES-staging/moh-playlist.txt
@@ -0,0 +1,5 @@
+Subject: res_musiconhold
+
+A new mode - playlist - has been added to res_musiconhold. This mode allows the
+user to specify the files (or URLs) to play explicitly by putting them directly
+in musiconhold.conf.
diff --git a/res/res_musiconhold.c b/res/res_musiconhold.c
index f7700758731fbaa89f3f384810f2adcb295519de..1bacb116a1fe5a4d827ef359bc24823e3bf5ecb3 100644
--- a/res/res_musiconhold.c
+++ b/res/res_musiconhold.c
@@ -1081,6 +1081,20 @@ static void moh_parse_options(struct ast_variable *var, struct mohclass *mohclas
 			ast_copy_string(mohclass->name, var->value, sizeof(mohclass->name));
 		} else if (!strcasecmp(var->name, "mode")) {
 			ast_copy_string(mohclass->mode, var->value, sizeof(mohclass->mode));
+		} else if (!strcasecmp(var->name, "entry")) {
+			if (ast_begins_with(var->value, "/") || ast_begins_with(var->value, "http://") || ast_begins_with(var->value, "https://")) {
+				char *dup = ast_strdup(var->value);
+				if (!dup) {
+					continue;
+				}
+				if (ast_begins_with(dup, "/") && strrchr(dup, '.')) {
+					ast_log(LOG_WARNING, "The playlist entry '%s' may include an extension, which could prevent it from playing.\n",
+						dup);
+				}
+				AST_VECTOR_APPEND(&mohclass->files, dup);
+			} else {
+				ast_log(LOG_ERROR, "Playlist entries must be a URL or absolute path, '%s' provided.\n", var->value);
+			}
 		} else if (!strcasecmp(var->name, "directory")) {
 			ast_copy_string(mohclass->dir, var->value, sizeof(mohclass->dir));
 		} else if (!strcasecmp(var->name, "application")) {
@@ -1130,6 +1144,8 @@ static void moh_parse_options(struct ast_variable *var, struct mohclass *mohclas
 			}
 		}
 	}
+
+	AST_VECTOR_COMPACT(&mohclass->files);
 }
 
 static int moh_scan_files(struct mohclass *class) {
@@ -1333,6 +1349,13 @@ static int _moh_register(struct mohclass *moh, int reload, int unref, const char
 			}
 			return -1;
 		}
+	} else if (!strcasecmp(moh->mode, "playlist")) {
+		if (!AST_VECTOR_SIZE(&moh->files)) {
+			if (unref) {
+				moh = mohclass_unref(moh, "unreffing potential new moh class (no playlist entries)");
+			}
+			return -1;
+		}
 	} else if (!strcasecmp(moh->mode, "mp3") || !strcasecmp(moh->mode, "mp3nb") ||
 			!strcasecmp(moh->mode, "quietmp3") || !strcasecmp(moh->mode, "quietmp3nb") ||
 			!strcasecmp(moh->mode, "httpmp3") || !strcasecmp(moh->mode, "custom")) {
@@ -1485,6 +1508,32 @@ static struct mohclass *_moh_class_malloc(const char *file, int line, const char
 static struct ast_variable *load_realtime_musiconhold(const char *name)
 {
 	struct ast_variable *var = ast_load_realtime("musiconhold", "name", name, SENTINEL);
+
+	if (var) {
+		const char *mode = ast_variable_find_in_list(var, "mode");
+		if (ast_strings_equal(mode, "playlist")) {
+			struct ast_variable *entries = ast_load_realtime("musiconhold_entry", "name", name, SENTINEL);
+			struct ast_variable *cur = entries;
+			size_t entry_count = 0;
+			for (; cur; cur = cur->next) {
+				if (!strcmp(cur->name, "entry")) {
+					struct ast_variable *dup = ast_variable_new(cur->name, cur->value, "");
+					if (dup) {
+						entry_count++;
+						ast_variable_list_append(&var, dup);
+					}
+				}
+			}
+			ast_variables_destroy(entries);
+
+			if (entry_count == 0) {
+				/* Behave as though this class doesn't exist */
+				ast_variables_destroy(var);
+				var = NULL;
+			}
+		}
+	}
+
 	if (!var) {
 		ast_log(LOG_WARNING,
 			"Music on Hold class '%s' not found in memory/database. "
@@ -1551,7 +1600,7 @@ static int local_ast_moh_start(struct ast_channel *chan, const char *mclass, con
 			ast_variables_destroy(var);
 
 			if (ast_strlen_zero(mohclass->dir)) {
-				if (!strcasecmp(mohclass->mode, "custom")) {
+				if (!strcasecmp(mohclass->mode, "custom") || !strcasecmp(mohclass->mode, "playlist")) {
 					strcpy(mohclass->dir, "nodir");
 				} else {
 					ast_log(LOG_WARNING, "A directory must be specified for class '%s'!\n", mohclass->name);
@@ -1605,6 +1654,11 @@ static int local_ast_moh_start(struct ast_channel *chan, const char *mclass, con
 						}
 						ast_set_flag(mohclass, MOH_RANDOMIZE);
 					}
+				} else if (!strcasecmp(mohclass->mode, "playlist")) {
+					if (!AST_VECTOR_SIZE(&mohclass->files)) {
+						mohclass = mohclass_unref(mohclass, "unreffing potential mohclass (no playlist entries)");
+						return -1;
+					}
 				} else if (!strcasecmp(mohclass->mode, "mp3") || !strcasecmp(mohclass->mode, "mp3nb") || !strcasecmp(mohclass->mode, "quietmp3") || !strcasecmp(mohclass->mode, "quietmp3nb") || !strcasecmp(mohclass->mode, "httpmp3") || !strcasecmp(mohclass->mode, "custom")) {
 
 					if (!strcasecmp(mohclass->mode, "custom"))
@@ -1846,7 +1900,7 @@ static int load_moh_classes(int reload)
 		ast_copy_string(class->name, cat, sizeof(class->name));
 
 		if (ast_strlen_zero(class->dir)) {
-			if (!strcasecmp(class->mode, "custom")) {
+			if (!strcasecmp(class->mode, "custom") || !strcasecmp(class->mode, "playlist")) {
 				strcpy(class->dir, "nodir");
 			} else {
 				ast_log(LOG_WARNING, "A directory must be specified for class '%s'!\n", class->name);