diff --git a/fillpdf.services.yml b/fillpdf.services.yml
index 7ae1660739b48d2ff61fbc99a2ac4c11ea020a76..14c609ff93f795c5ebc48985f6fcf8c4515334cf 100644
--- a/fillpdf.services.yml
+++ b/fillpdf.services.yml
@@ -20,3 +20,11 @@ services:
   plugin.manager.fillpdf_action.processor:
     class: Drupal\fillpdf\Plugin\FillPdfActionPluginManager
     parent: default_plugin_manager
+
+  fillpdf.output_handler:
+    class: Drupal\fillpdf\OutputHandler
+    arguments: ['@token', '@logger.channel.fillpdf']
+
+  logger.channel.fillpdf:
+    parent: logger.channel_base
+    arguments: ['fillpdf']
diff --git a/src/Component/Utility/FillPdf.php b/src/Component/Utility/FillPdf.php
index 0896e786a5b682a7eb3cdf3ff0c8055645709d55..05834bcedf4a03a4a97c4ad2a2c6e3b388e2c127 100644
--- a/src/Component/Utility/FillPdf.php
+++ b/src/Component/Utility/FillPdf.php
@@ -47,7 +47,15 @@ class FillPdf {
   }
 
   /**
-   * Constructs a URI to FillPDF's default files location given a relative path.
+   * Constructs a URI to a location given a relative path.
+   *
+   * @param string $scheme
+   *   A valid stream wrapper, such as 'public' or 'private'
+   *
+   * @param $path
+   *   The path component that should come after the stream wrapper.
+   *
+   * @return string
    */
   public static function buildFileUri($scheme, $path) {
     $uri = $scheme . '://' . $path;
diff --git a/src/Controller/HandlePdfController.php b/src/Controller/HandlePdfController.php
index 4ca0ee815eb97836ba129a07672b66cb2e0fff5b..1f400faccf4e4e784c8a8ca0df2ad92cda83a996 100644
--- a/src/Controller/HandlePdfController.php
+++ b/src/Controller/HandlePdfController.php
@@ -126,18 +126,15 @@ class HandlePdfController extends ControllerBase {
             // @todo: What if one fill pattern has tokens from multiple types in it? Figure out the best way to deal with that and rewrite this section accordingly. Probably some form of parallel arrays. Basically we'd have to run all combinations, although our logic still might not be smart enough to tell if *all* tokens in the source text have been replaced, or in which case both of them have been replaced last (which is what we want). I could deliberately pass each entity context separately and then count how many of them match, and only overwrite it if the match count is higher than the current one. Yeah, that's kind of inefficient but also a good start. I might just be able to scan for tokens myself and then check if they're still in the $uncleaned_base output, or do the cleaning myself so I only have to call Token::replace once. TBD.
             $field_pattern = $field->value->value;
             $maybe_replaced_string = $this->token->replace($field_pattern, [
-              $entity_type => $entity
+              $entity_type => $entity,
             ], [
               'clean' => TRUE,
-              'sanitize' => FALSE,
             ]);
             // Generate a non-cleaned version of the token string so we can
             // tell if the non-empty string we got back actually replaced
             // some tokens.
             $uncleaned_base = $this->token->replace($field_pattern, [
-              $entity_type => $entity
-            ], [
-              'sanitize' => FALSE,
+              $entity_type => $entity,
             ]);
 
             // If we got a result that isn't what we put in, update the value
@@ -156,7 +153,7 @@ class HandlePdfController extends ControllerBase {
 
     // @todo: When Rules integration ported, emit an event or whatever.
 
-    // TODO: figure out what to do about $token_objects. Should I make buildObjects manually re-run everything or just use the final entities passed of each type? Maybe just the latter, since that is what I do in
+    // TODO: figure out what to do about $token_objects. Should I make buildFilename manually re-run everything or just use the final entities passed of each type? Maybe just the latter, since that is what I do in
     $action_response =  $this->handlePopulatedPdf($fillpdf_form, $populated_pdf, $context, []);
 
     return $action_response;
@@ -221,7 +218,7 @@ class HandlePdfController extends ControllerBase {
       'context' => $context,
       'token_objects' => $token_objects,
       'data' => $pdf_data,
-      'generated_filename' => $output_name,
+      'filename' => $output_name,
     ];
 
     /** @var FillPdfActionPluginInterface $fillpdf_action */
diff --git a/src/OutputHandler.php b/src/OutputHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..c8fea191d5c6071014ace69b0e32c03609506a16
--- /dev/null
+++ b/src/OutputHandler.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\fillpdf\OutputHandler.
+ */
+
+namespace Drupal\fillpdf;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Utility\Token;
+use Drupal\file\FileInterface;
+use Drupal\file\FileUsage\FileUsageInterface;
+use Drupal\fillpdf\Component\Utility\FillPdf;
+use Drupal\fillpdf\Entity\FillPdfForm;
+use Psr\Log\LoggerInterface;
+
+
+/**
+ * Class OutputHandler.
+ *
+ * @package Drupal\fillpdf
+ */
+class OutputHandler implements OutputHandlerInterface {
+
+  use StringTranslationTrait;
+
+  /** @var Token $token */
+  protected $token;
+
+  /** @var \Psr\Log\LoggerInterface $logger */
+  protected $logger;
+
+  /** @var \Drupal\file\FileUsage\FileUsageInterface $fileUsage */
+  protected $fileUsage;
+
+  /**
+   * OutputHandler constructor.
+   */
+  public function __construct(FileUsageInterface $file_usage, Token $token, LoggerInterface $logger) {
+    $this->fileUsage = $file_usage;
+    $this->token = $token;
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function savePdfToFile($pdf_data, array $context, $destination_path_override = NULL) {
+    /** @var FillPdfForm $fillpdf_form */
+    $fillpdf_form = $context['form'];
+
+    /** @var array $token_objects */
+    $token_objects = $context['token_objects'];
+
+    $destination_path = 'fillpdf';
+    if (!empty($fillpdf_form->destination_path->value)) {
+      $destination_path = "fillpdf/{$destination_path}";
+    }
+    if (!empty($destination_path_override)) {
+      $destination_path = "fillpdf/{$destination_path_override}";
+    }
+
+    $resolved_destination_path = $this->processDestinationPath($destination_path, $token_objects, $fillpdf_form->scheme->value);
+    $path_exists = file_prepare_directory($resolved_destination_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    $saved_file = FALSE;
+    if ($path_exists === FALSE) {
+      $this->logger->critical($this->t("The path %destination_path does not exist and could not be
+      automatically created. Therefore, the previous submission was not saved. If
+      the URL contained download=1, then the PDF was still sent to the user's browser.
+      If you were redirecting them to the PDF, they were sent to the homepage instead.
+      If the destination path looks wrong and you have used tokens, check that you have
+      used the correct token and that it is available to FillPDF at the time of PDF
+      generation.",
+        ['%destination_path' => $resolved_destination_path]));
+    }
+    else {
+      // Full steam ahead!
+      $saved_file = file_save_data($pdf_data, "{$resolved_destination_path}/{$context['filename']}", FILE_EXISTS_RENAME);
+      $this->addFileUsage($saved_file, 'fillpdf_file');
+    }
+
+    return $saved_file;
+  }
+
+  /**
+   * @param string $destination_path
+   * @param array $token_objects
+   * @param string $scheme
+   * @return string
+   */
+  protected function processDestinationPath($destination_path, $token_objects, $scheme = 'public') {
+    $orig_path = $destination_path;
+    $destination_path = trim($orig_path);
+    // Replace any applicable tokens
+    $types = [];
+    if (isset($token_objects['node'])) {
+      $types[] = 'node';
+    }
+    elseif (isset($token_objects['webform'])) {
+      $types[] = 'webform';
+    }
+    // TODO: Do this kind of replacement with a common service instead, because I'm doing the same thing in like 3 places now.
+    foreach ($types as $type) {
+      $destination_path = $this->token->replace($destination_path, [$type => $token_objects[$type]], ['clear' => TRUE]);
+    }
+
+    // Slap on the files directory in front and return it
+    $destination_path = FillPdf::buildFileUri($scheme, $destination_path);
+    return $destination_path;
+  }
+
+  /**
+   * @param \Drupal\file\FileInterface $fillpdf_file
+   * @param array $context
+   *   An array of the entities that were used to generate this file.
+   */
+  protected function addFileUsage(FileInterface $fillpdf_file, array $context) {
+    // TODO: Add FillPdfFileContext content entity
+//    $fillpdf_file_context = FillPdfFileContext::create();
+//    $this->fileUsage->add($fillpdf_file, 'fillpdf', 'fillpdf_file', $fillpdf_file_context->fcid);
+  }
+
+}
diff --git a/src/OutputHandlerInterface.php b/src/OutputHandlerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..6eba1173ace9626904871fe4f9852ff872f2eaec
--- /dev/null
+++ b/src/OutputHandlerInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\fillpdf\OutputHandlerInterface.
+ */
+
+namespace Drupal\fillpdf;
+use Drupal\file\Entity\File;
+
+/**
+ * Contains functions to standardize output handling for generated PDFs.
+ *
+ * @package Drupal\fillpdf
+ */
+interface OutputHandlerInterface {
+
+  /**
+   * @param string $pdf_data
+   *   A string containing the full contents of the PDF to be saved.
+   * @param array $context
+   *   An array containing the following properties:
+   *     form: The FillPdfForm object from which the PDF was generated.
+   *     context: The FillPDF request context as returned by
+   *       \Drupal\fillpdf\FillPdfLinkParserInterface.
+   *     token_objects: The token data from which the PDF was generated.
+   *     data: The populated PDF data itself.
+   *     filename: The filename (not including path) with which
+   *       the PDF should be presented.
+   *
+   * @param string $destination_path_override
+   * @return bool|\Drupal\file\Entity\File
+   */
+  public function savePdfToFile($pdf_data, array $context, $destination_path_override = NULL);
+
+}
diff --git a/src/Plugin/FillPdfActionPlugin/FillPdfSaveAction.php b/src/Plugin/FillPdfActionPlugin/FillPdfSaveAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5357c49011fd83eb78eb86014a824292e35cbb4
--- /dev/null
+++ b/src/Plugin/FillPdfActionPlugin/FillPdfSaveAction.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\fillpdf\Plugin\FillPdfActionPlugin\FillPdfDownloadAction.
+ */
+
+namespace Drupal\fillpdf\Plugin\FillPdfActionPlugin;
+
+use Drupal\Core\Annotation\Translation;
+use Drupal\fillpdf\Annotation\FillPdfActionPlugin;
+use Drupal\fillpdf\OutputHandler;
+use Drupal\fillpdf\Plugin\FillPdfActionPluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\ResponseHeaderBag;
+
+/**
+ * Class FillPdfDownloadAction
+ * @package Drupal\fillpdf\Plugin\FillPdfActionPlugin
+ *
+ * @FillPdfActionPlugin(
+ *   id = "save",
+ *   label = @Translation("Save PDF to file")
+ * )
+ */
+class FillPdfDownloadAction extends FillPdfActionPluginBase {
+
+  /** @var OutputHandler $outputHandler */
+  protected $outputHandler;
+
+  public function __construct(OutputHandler $output_handler, array $configuration, $plugin_id, $plugin_definition) {
+    $this->outputHandler = $output_handler;
+
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static($container->get('fillpdf.output_handler'), $configuration, $plugin_id, $plugin_definition);
+  }
+
+  public function execute() {
+    // @todo: Error handling?
+    $this->outputHandler->savePdfToFile($this->configuration['data'], $this->configuration['context']);
+
+    // @todo: Fix based on value of post_save_redirect, once I add that
+    $response = new RedirectResponse('/');
+    return $response;
+  }
+
+}