diff --git a/bin/run_xslt_scripts.sh b/bin/run_xslt_scripts.sh
new file mode 100755
index 0000000..bc6b39f
--- /dev/null
+++ b/bin/run_xslt_scripts.sh
@@ -0,0 +1,382 @@
+#!/bin/bash
+
+#######################################################################
+# Copyright 2017 Internet2
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#######################################################################
+
+#######################################################################
+# Help message
+#######################################################################
+
+display_help () {
+/bin/cat <<- HELP_MSG
+	This script applies an arbitrary sequence of XSLT scripts
+	against a SAML metadata aggregate. The output files will 
+	be written to the output directory specified on the 
+	command line.
+	
+	The script depends on cached metadata. It will not fetch
+	a metadata file from the server.
+	
+	Usage: ${0##*/} [-hv] -u MD_LOCATION -d OUT_DIR "XSL_FILENAME [OUTPUT_FILENAME]" ...
+	
+	The script takes an arbitrary sequence of compound command-line
+	arguments. The second sub-argument of each compound argument is
+	OPTIONAL. If no OUTPUT_FILENAME is given for a particular
+	XSL_FILENAME, the script computes an output filename as best
+	it can.
+	
+	Options:
+	   -h      Display this help message
+	   -v      Enable DEBUG mode
+	   -u      Specify the metadata location
+	   -d      Specify the output directory
+
+	Option -h is mutually exclusive of all other options.
+	
+	Option -u specifies the location of the metadata file on the 
+	network. The script uses the location to retrieve the file 
+	from cache. If the file is not cached, the script will fail.
+	This option is REQUIRED.
+	
+	Option -d specifies the ultimate output directory, which is
+	usually a web directory. This option is REQUIRED.
+	
+	Output is all or none. The output files are computed
+	sequentially and assembled one-by-one in a temporary 
+	directory. All of the output files are moved to the 
+	destination directory at the same time. If even one output 
+	file can not be computed---for whatever reason---none are
+	written to the output directory.
+	
+	ENVIRONMENT
+	
+	This script leverages a handful of environment variables:
+	
+	  LIB_DIR    A source library directory
+	  CACHE_DIR  A persistent HTTP cache
+	  TMPDIR     A temporary directory
+	  LOG_FILE   A persistent log file
+	  LOG_LEVEL  The global log level [0..5]
+	
+	All of the above environment variables are REQUIRED
+	except LOG_LEVEL, which defaults to LOG_LEVEL=3.
+	
+	The following environment variables are REQUIRED:
+	
+	$( printf "  %s\n" ${env_vars[*]} )
+	
+	The following directories MUST exist:
+	
+	$( printf "  %s\n" ${dir_paths[*]} )
+	
+	The following files MUST exist:
+	
+	$( printf "  %s\n" $LOG_FILE )
+	
+	CONFIGURATION
+	
+	The following source library files MUST be installed in LIB_DIR:
+	
+	$( printf "  %s\n" ${lib_filenames[*]} )
+	
+	EXAMPLES
+	
+	  \$ ${0##*/} -h
+	  \$ md_location=http://md.incommon.org/InCommon/InCommon-metadata-export.xml
+	  \$ out_dir=/home/htdocs/www.incommonfederation.org/federation/metadata/
+	  \$ ${0##*/} -u \$md_location -d \$out_dir \\
+	        "list_all_IdPs_csv.xsl all_IdPs_exported.csv" \\
+	        "list_all_SPs_csv.xsl all_SPs_exported.csv"
+HELP_MSG
+}
+
+#######################################################################
+# Bootstrap
+#######################################################################
+
+script_name=${0##*/}  # equivalent to basename $0
+
+# required environment variables
+env_vars[1]="LIB_DIR"
+env_vars[2]="CACHE_DIR"
+env_vars[3]="TMPDIR"
+env_vars[4]="LOG_FILE"
+
+# check environment variables
+for env_var in ${env_vars[*]}; do
+	eval "env_var_val=\${$env_var}"
+	if [ -z "$env_var_val" ]; then
+		echo "ERROR: $script_name requires env var $env_var" >&2
+		exit 2
+	fi
+done
+
+# required directories
+dir_paths[1]="$LIB_DIR"
+dir_paths[2]="$CACHE_DIR"
+dir_paths[3]="$TMPDIR"
+
+# check required directories
+for dir_path in ${dir_paths[*]}; do
+	if [ ! -d "$dir_path" ]; then
+		echo "ERROR: $script_name: directory does not exist: $dir_path" >&2
+		exit 2
+	fi
+done
+
+# check the log file
+# devices such as /dev/tty and /dev/null are allowed
+if [ ! -f "$LOG_FILE" ] && [[ $LOG_FILE != /dev/* ]]; then
+	echo "ERROR: $script_name: file does not exist: $LOG_FILE" >&2
+	exit 2
+fi
+
+# default to INFO logging
+if [ -z "$LOG_LEVEL" ]; then
+	LOG_LEVEL=3
+fi
+
+# library filenames
+lib_filenames[1]="core_lib.sh"
+lib_filenames[2]="http_tools.sh"
+
+# check lib files
+for lib_filename in ${lib_filenames[*]}; do
+	lib_file="$LIB_DIR/$lib_filename"
+	if [ ! -f "$lib_file" ]; then
+		echo "ERROR: $script_name: lib file does not exist: $lib_file" >&2
+		exit 2
+	fi
+done
+
+#######################################################################
+# Process command-line options and arguments
+#######################################################################
+
+help_mode=false; local_opts=
+while getopts ":hvu:d:" opt; do
+	case $opt in
+		h)
+			help_mode=true
+			;;
+		v)
+			LOG_LEVEL=4
+			local_opts="$local_opts -$opt"
+			;;
+		u)
+			md_location="$OPTARG"
+			;;
+		d)
+			out_dir="$OPTARG"
+			;;
+		\?)
+			echo "ERROR: $script_name: Unrecognized option: -$OPTARG" >&2
+			exit 2
+			;;
+		:)
+			echo "ERROR: $script_name: Option -$OPTARG requires an argument" >&2
+			exit 2
+			;;
+	esac
+done
+
+if $help_mode; then
+	display_help
+	exit 0
+fi
+
+# check the metadata location
+if [ -z "$md_location" ]; then
+	echo "ERROR: $script_name: no metadata location specified (option -u)" >&2
+	exit 2
+fi
+
+# check the output directory
+if [ -z "$out_dir" ]; then
+	echo "ERROR: $script_name: no output directory specified (option -d)" >&2
+	exit 2
+fi
+if [ ! -d "$out_dir" ]; then
+	echo "ERROR: $script_name: directory does not exist: $out_dir" >&2
+	exit 2
+fi
+
+# check command-line arguments
+shift $(( OPTIND - 1 ))
+if [ $# -eq 0 ]; then
+	echo "ERROR: $script_name: wrong number of arguments: $# (at least one required)" >&2
+	exit 2
+fi
+
+#######################################################################
+# Functions
+#######################################################################
+
+# process a compound command-line argument
+# present that argument to this function as a pair of arguments:
+#   XSL_FILENAME [OUTPUT_FILENAME]
+# if the latter is omitted, the function computes a suitable output filename
+process_arg () {
+
+	local xsl_file
+	local base_filename
+	local output_filename
+	
+	# check the command-line arguments
+	if [ $# -lt 1 -o $# -gt 2 ]; then
+		echo "ERROR: $FUNCNAME: incorrect number of arguments: $# (1 or 2 required)" >&2
+		return 2
+	fi
+		
+	# the first arg is an XSL filename
+	# the second arg is an (optional) output filename
+	
+    # does the first arg end in .xsl?
+    if [[ $1 != *.xsl ]]; then
+		echo "ERROR: $FUNCNAME: XSL filename does not end in .xsl: $1" >&2
+		return 3
+    fi
+    
+    # does the corresponding file exist?
+	xsl_file="$LIB_DIR/$1"
+	if [ ! -f "$xsl_file" ]; then
+		echo "ERROR: $FUNCNAME: lib file does not exist: $xsl_file" >&2
+		exit 4
+	fi
+    
+    # capture array elements
+    xsl_filenames+=("$1")
+    xsl_files+=("$xsl_file")
+
+	# determine the output filename
+	output_filename="$2"
+	if [ -z "$output_filename" ]; then
+	
+		# compute the output filename
+		
+		# if the XSL filename is of the form
+		#   base_filename_csv.xsl
+		# or
+		#   base_filename_json.xsl
+		# then construct the output filename with the corresponding extension:
+		#   base_filename.csv
+		#   base_filename_.son
+		# otherwise the output filename is the XSL filename sans the .xsl extension
+		base_filename="${1%%.*}"
+		if [[ $base_filename =~ _(csv|json)$ ]]; then
+			output_filename=$( echo "$base_filename" | $_SED -e 's/^\(.*\)_\([^_]*\)$/\1.\2/' )
+		else
+			output_filename="$base_filename"
+		fi
+	fi
+    
+    # capture remaining array elements
+    out_filenames+=("$output_filename")
+    out_files+=("${tmp_dir}/$output_filename")
+}
+
+#######################################################################
+# Initialization
+#######################################################################
+
+# source lib files
+for lib_filename in ${lib_filenames[*]}; do
+	lib_file="$LIB_DIR/$lib_filename"
+	source "$lib_file"
+	status_code=$?
+	if [ $status_code -ne 0 ]; then
+		echo "ERROR: $script_name failed ($status_code) to source lib file $lib_file" >&2
+		exit 2
+	fi
+done
+
+# create a temporary subdirectory
+tmp_dir="${TMPDIR%%/}/${script_name%%.*}_$$"
+/bin/mkdir "$tmp_dir"
+status_code=$?
+if [ $status_code -ne 0 ]; then
+	echo "ERROR: $script_name failed ($status_code) to create tmp dir $tmp_dir" >&2
+	exit 2
+fi
+
+# specify input file
+xml_file="${tmp_dir}/saml-metadata.xml"
+
+# declare arrays
+xsl_filenames=()
+xsl_files=()
+out_filenames=()
+out_files=()
+
+# iterate over the command-line arguments
+num_args=$#  # TODO: simplify this loop
+for (( i = 0; i < num_args; i++ )); do
+
+	# process the next command-line arg
+	# (do not quote the arg)
+	process_arg $1
+	status_code=$?
+	if [ $status_code -ne 0 ]; then
+		echo "ERROR: $script_name failed ($status_code) to process arg $1" >&2
+		exit 2
+	fi
+	
+	shift 
+done
+
+#######################################################################
+# Main processing
+#######################################################################
+
+print_log_message -I "$script_name BEGIN"
+
+# get a cached metadata file
+print_log_message -I "$script_name retrieving cached metadata file: $md_location"
+conditional_get $local_opts -C -d "$CACHE_DIR" -T "$tmp_dir" "$md_location" > "$xml_file"
+status_code=$?
+if [ $status_code -eq 1 ]; then
+	# metadata must be cached
+	print_log_message -E "$script_name: metadata file not cached: $md_location"
+	clean_up_and_exit -d "$tmp_dir" 1
+fi
+if [ $status_code -gt 1 ]; then
+	print_log_message -E "$script_name: conditional_get failed ($status_code) on location: $md_location"
+    clean_up_and_exit -d "$tmp_dir" $status_code
+fi
+print_log_message -D "$script_name using XML file: $xml_file"
+
+# create the output files
+for i in ${!out_files[*]}; do
+	print_log_message -I "$script_name writing output file: ${out_filenames[$i]}"
+	/usr/bin/xsltproc ${xsl_files[$i]} $xml_file > ${out_files[$i]}
+	status_code=$?
+	if [ $status_code -ne 0 ]; then
+		print_log_message -E "$script_name: xsltproc failed ($status_code) on stylesheet: ${xsl_files[$i]}"
+		clean_up_and_exit -d "$tmp_dir" $status_code
+	fi
+done
+
+# move the output files to the web directory
+print_log_message -I "$script_name moving output files to dir: $out_dir"
+/bin/mv $( echo -n ${out_files[*]} ) $out_dir
+status_code=$?
+if [ $status_code -ne 0 ]; then
+	print_log_message -E "$script_name: mv failed ($status_code) to dir: $out_dir"
+    clean_up_and_exit -d "$tmp_dir" $status_code
+fi
+
+print_log_message -I "$script_name END"
+clean_up_and_exit -d "$tmp_dir" 0
diff --git a/install.sh b/install.sh
index 102cb02..7a8aa4d 100755
--- a/install.sh
+++ b/install.sh
@@ -93,8 +93,7 @@ while read script_file; do
 	fi
 done <<SCRIPTS
 $script_bin/bin/http_xsltproc.sh
-$script_bin/bin/process_export_aggregate.sh
-$script_bin/bin/process_main_aggregate.sh
+$script_bin/bin/run_xslt_scripts.sh
 SCRIPTS
 
 # initialize lib dir