diff --git a/resources/sql/autopatches/20150814.harbormater.artifact.phid.sql b/resources/sql/autopatches/20150814.harbormater.artifact.phid.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20150814.harbormater.artifact.phid.sql
@@ -0,0 +1,5 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildartifact
+  ADD phid VARBINARY(64) NOT NULL AFTER id;
+
+UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildartifact
+  SET phid = CONCAT('PHID-HMBA-', id) WHERE phid = '';
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -915,12 +915,14 @@
     'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php',
     'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php',
     'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php',
+    'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php',
     'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php',
     'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
     'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
     'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
     'HarbormasterBuildArcanistAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildArcanistAutoplan.php',
     'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
+    'HarbormasterBuildArtifactPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php',
     'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
     'HarbormasterBuildAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php',
     'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
@@ -980,9 +982,12 @@
     'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php',
     'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
     'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
+    'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php',
     'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
     'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php',
+    'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
+    'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php',
     'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php',
     'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
     'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
@@ -1018,6 +1023,7 @@
     'HarbormasterTestBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterTestBuildStepGroup.php',
     'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
     'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
+    'HarbormasterURIArtifact' => 'applications/harbormaster/artifact/HarbormasterURIArtifact.php',
     'HarbormasterUnitMessagesController' => 'applications/harbormaster/controller/HarbormasterUnitMessagesController.php',
     'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php',
     'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
@@ -4603,6 +4609,7 @@
     'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
+    'HarbormasterArtifact' => 'Phobject',
     'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase',
     'HarbormasterBuild' => array(
       'HarbormasterDAO',
@@ -4616,6 +4623,7 @@
       'HarbormasterDAO',
       'PhabricatorPolicyInterface',
     ),
+    'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType',
     'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'HarbormasterBuildAutoplan' => 'Phobject',
     'HarbormasterBuildCommand' => 'HarbormasterDAO',
@@ -4700,9 +4708,12 @@
     'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
     'HarbormasterController' => 'PhabricatorController',
+    'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
     'HarbormasterDAO' => 'PhabricatorLiskDAO',
     'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup',
+    'HarbormasterFileArtifact' => 'HarbormasterArtifact',
     'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
+    'HarbormasterHostArtifact' => 'HarbormasterArtifact',
     'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
     'HarbormasterLintMessagesController' => 'HarbormasterController',
     'HarbormasterLintPropertyView' => 'AphrontView',
@@ -4738,6 +4749,7 @@
     'HarbormasterTestBuildStepGroup' => 'HarbormasterBuildStepGroup',
     'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
     'HarbormasterUIEventListener' => 'PhabricatorEventListener',
+    'HarbormasterURIArtifact' => 'HarbormasterArtifact',
     'HarbormasterUnitMessagesController' => 'HarbormasterController',
     'HarbormasterUnitPropertyView' => 'AphrontView',
     'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
diff --git a/src/applications/drydock/query/DrydockLeaseQuery.php b/src/applications/drydock/query/DrydockLeaseQuery.php
--- a/src/applications/drydock/query/DrydockLeaseQuery.php
+++ b/src/applications/drydock/query/DrydockLeaseQuery.php
@@ -27,19 +27,12 @@
     return $this;
   }
 
+  public function newResultObject() {
+    return new DrydockLease();
+  }
+
   protected function loadPage() {
-    $table = new DrydockLease();
-    $conn_r = $table->establishConnection('r');
-
-    $data = queryfx_all(
-      $conn_r,
-      'SELECT lease.* FROM %T lease %Q %Q %Q',
-      $table->getTableName(),
-      $this->buildWhereClause($conn_r),
-      $this->buildOrderClause($conn_r),
-      $this->buildLimitClause($conn_r));
-
-    return $table->loadAllFromArray($data);
+    return $this->loadStandardPage($this->newResultObject());
   }
 
   protected function willFilterPage(array $leases) {
@@ -69,40 +62,38 @@
     return $leases;
   }
 
-  protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
-    $where = array();
+  protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+    $where = parent::buildWhereClauseParts($conn);
 
-    if ($this->resourceIDs) {
+    if ($this->resourceIDs !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'resourceID IN (%Ld)',
         $this->resourceIDs);
     }
 
-    if ($this->ids) {
+    if ($this->ids !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'id IN (%Ld)',
         $this->ids);
     }
 
-    if ($this->phids) {
+    if ($this->phids !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'phid IN (%Ls)',
         $this->phids);
     }
 
-    if ($this->statuses) {
+    if ($this->statuses !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'status IN (%Ld)',
         $this->statuses);
     }
 
-    $where[] = $this->buildPagingClause($conn_r);
-
-    return $this->formatWhereClause($where);
+    return $where;
   }
 
 }
diff --git a/src/applications/harbormaster/artifact/HarbormasterArtifact.php b/src/applications/harbormaster/artifact/HarbormasterArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterArtifact.php
@@ -0,0 +1,88 @@
+<?php
+
+
+abstract class HarbormasterArtifact extends Phobject {
+
+  private $buildArtifact;
+
+  abstract public function getArtifactTypeName();
+
+  public function getArtifactTypeSummary() {
+    return $this->getArtifactTypeDescription();
+  }
+
+  abstract public function getArtifactTypeDescription();
+  abstract public function getArtifactParameterSpecification();
+  abstract public function getArtifactParameterDescriptions();
+  abstract public function willCreateArtifact(PhabricatorUser $actor);
+
+  public function validateArtifactData(array $artifact_data) {
+    $artifact_spec = $this->getArtifactParameterSpecification();
+    PhutilTypeSpec::checkMap($artifact_data, $artifact_spec);
+  }
+
+  public function renderArtifactSummary(PhabricatorUser $viewer) {
+    return null;
+  }
+
+  public function releaseArtifact(PhabricatorUser $actor) {
+    return;
+  }
+
+  public function getArtifactDataExample() {
+    return null;
+  }
+
+  public function setBuildArtifact(HarbormasterBuildArtifact $build_artifact) {
+    $this->buildArtifact = $build_artifact;
+    return $this;
+  }
+
+  public function getBuildArtifact() {
+    return $this->buildArtifact;
+  }
+
+  final public function getArtifactConstant() {
+    $class = new ReflectionClass($this);
+
+    $const = $class->getConstant('ARTIFACTCONST');
+    if ($const === false) {
+      throw new Exception(
+        pht(
+          '"%s" class "%s" must define a "%s" property.',
+          __CLASS__,
+          get_class($this),
+          'ARTIFACTCONST'));
+    }
+
+    $limit = self::getArtifactConstantByteLimit();
+    if (!is_string($const) || (strlen($const) > $limit)) {
+      throw new Exception(
+        pht(
+          '"%s" class "%s" has an invalid "%s" property. Action constants '.
+          'must be strings and no more than %s bytes in length.',
+          __CLASS__,
+          get_class($this),
+          'ARTIFACTCONST',
+          new PhutilNumber($limit)));
+    }
+
+    return $const;
+  }
+
+  final public static function getArtifactConstantByteLimit() {
+    return 32;
+  }
+
+  final public static function getAllArtifactTypes() {
+    return id(new PhutilClassMapQuery())
+      ->setAncestorClass(__CLASS__)
+      ->setUniqueMethod('getArtifactConstant')
+      ->execute();
+  }
+
+  final public static function getArtifactType($type) {
+    return idx(self::getAllArtifactTypes(), $type);
+  }
+
+}
diff --git a/src/applications/harbormaster/artifact/HarbormasterFileArtifact.php b/src/applications/harbormaster/artifact/HarbormasterFileArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterFileArtifact.php
@@ -0,0 +1,66 @@
+<?php
+
+final class HarbormasterFileArtifact extends HarbormasterArtifact {
+
+  const ARTIFACTCONST = 'file';
+
+  public function getArtifactTypeName() {
+    return pht('File');
+  }
+
+  public function getArtifactTypeDescription() {
+    return pht(
+      'Stores a reference to file data which has been uploaded to '.
+      'Phabricator.');
+  }
+
+  public function getArtifactParameterSpecification() {
+    return array(
+      'filePHID' => 'string',
+    );
+  }
+
+  public function getArtifactParameterDescriptions() {
+    return array(
+      'filePHID' => pht('File to create an artifact from.'),
+    );
+  }
+
+  public function getArtifactDataExample() {
+    return array(
+      'filePHID' => 'PHID-FILE-abcdefghijklmnopqrst',
+    );
+  }
+
+  public function renderArtifactSummary(PhabricatorUser $viewer) {
+    $artifact = $this->getBuildArtifact();
+    $file_phid = $artifact->getProperty('filePHID');
+    return $viewer->renderHandle($file_phid);
+  }
+
+  public function willCreateArtifact(PhabricatorUser $actor) {
+    // NOTE: This is primarily making sure the actor has permission to view the
+    // file. We don't want to let you run builds using files you don't have
+    // permission to see, since this could let you violate permissions.
+    $this->loadArtifactFile($actor);
+  }
+
+  public function loadArtifactFile(PhabricatorUser $viewer) {
+    $artifact = $this->getBuildArtifact();
+    $file_phid = $artifact->getProperty('filePHID');
+
+    $file = id(new PhabricatorFileQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($file_phid))
+      ->executeOne();
+    if (!$file) {
+      throw new Exception(
+        pht(
+          'File PHID "%s" does not correspond to a valid file.',
+          $file_phid));
+    }
+
+    return $file;
+  }
+
+}
diff --git a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php
@@ -0,0 +1,74 @@
+<?php
+
+final class HarbormasterHostArtifact extends HarbormasterArtifact {
+
+  const ARTIFACTCONST = 'host';
+
+  public function getArtifactTypeName() {
+    return pht('Drydock Host');
+  }
+
+  public function getArtifactTypeDescription() {
+    return pht('References a host lease from Drydock.');
+  }
+
+
+  public function getArtifactParameterSpecification() {
+    return array(
+      'drydockLeasePHID' => 'string',
+    );
+  }
+
+  public function getArtifactParameterDescriptions() {
+    return array(
+      'drydockLeasePHID' => pht(
+        'Drydock host lease to create an artifact from.'),
+    );
+  }
+
+  public function getArtifactDataExample() {
+    return array(
+      'drydockLeasePHID' => 'PHID-DRYL-abcdefghijklmnopqrst',
+    );
+  }
+
+  public function renderArtifactSummary(PhabricatorUser $viewer) {
+    $artifact = $this->getBuildArtifact();
+    $file_phid = $artifact->getProperty('drydockLeasePHID');
+    return $viewer->renderHandle($file_phid);
+  }
+
+  public function willCreateArtifact(PhabricatorUser $actor) {
+    $this->loadArtifactLease($actor);
+  }
+
+  public function loadArtifactLease(PhabricatorUser $viewer) {
+    $artifact = $this->getBuildArtifact();
+    $lease_phid = $artifact->getProperty('drydockLeasePHID');
+
+    $lease = id(new DrydockLeaseQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($lease_phid))
+      ->executeOne();
+    if (!$lease) {
+      throw new Exception(
+        pht(
+          'Drydock lease PHID "%s" does not correspond to a valid lease.',
+          $lease_phid));
+    }
+
+    return $lease;
+  }
+
+  public function releaseArtifact(PhabricatorUser $actor) {
+    $lease = $this->loadArtifactLease($actor);
+    $resource = $lease->getResource();
+    $blueprint = $resource->getBlueprint();
+
+    if ($lease->isActive()) {
+      $blueprint->releaseLease($resource, $lease);
+    }
+  }
+
+
+}
diff --git a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php
@@ -0,0 +1,100 @@
+<?php
+
+final class HarbormasterURIArtifact extends HarbormasterArtifact {
+
+  const ARTIFACTCONST = 'uri';
+
+  public function getArtifactTypeName() {
+    return pht('URI');
+  }
+
+  public function getArtifactTypeSummary() {
+    return pht('Stores a URI.');
+  }
+
+  public function getArtifactTypeDescription() {
+    return pht(
+      "Stores a URI.\n\n".
+      "With `ui.external`, you can use this artifact type to add links to ".
+      "build results in an external build system.");
+  }
+
+  public function getArtifactParameterSpecification() {
+    return array(
+      'uri' => 'string',
+      'name' => 'optional string',
+      'ui.external' => 'optional bool',
+    );
+  }
+
+  public function getArtifactParameterDescriptions() {
+    return array(
+      'uri' => pht('The URI to store.'),
+      'name' => pht('Optional label for this URI.'),
+      'ui.external' => pht(
+        'If true, display this URI in the UI as an link to '.
+        'additional build details in an external build system.'),
+    );
+  }
+
+  public function getArtifactDataExample() {
+    return array(
+      'uri' => 'https://buildserver.mycompany.com/build/123/',
+      'name' => pht('View External Build Results'),
+      'ui.external' => true,
+    );
+  }
+
+  public function renderArtifactSummary(PhabricatorUser $viewer) {
+    $artifact = $this->getBuildArtifact();
+    $uri = $artifact->getProperty('uri');
+
+    try {
+      $this->validateURI($uri);
+    } catch (Exception $ex) {
+      return pht('<Invalid URI>');
+    }
+
+    $name = $artifact->getProperty('name', $uri);
+
+    return phutil_tag(
+      'a',
+      array(
+        'href' => $uri,
+        'target' => '_blank',
+      ),
+      $name);
+  }
+
+  public function willCreateArtifact(PhabricatorUser $actor) {
+    $artifact = $this->getBuildArtifact();
+    $uri = $artifact->getProperty('uri');
+    $this->validateURI($uri);
+  }
+
+  private function validateURI($raw_uri) {
+    $uri = new PhutilURI($raw_uri);
+
+    $protocol = $uri->getProtocol();
+    if (!strlen($protocol)) {
+      throw new Exception(
+        pht(
+          'Unable to identify the protocol for URI "%s". URIs must be '.
+          'fully qualified and have an identifiable protocol.',
+          $raw_uri));
+    }
+
+    $protocol_key = 'uri.allowed-protocols';
+    $protocols = PhabricatorEnv::getEnvConfig($protocol_key);
+    if (empty($protocols[$protocol])) {
+      throw new Exception(
+        pht(
+          'URI "%s" does not have an allowable protocol. Configure '.
+          'protocols in `%s`. Allowed protocols are: %s.',
+          $raw_uri,
+          $protocol_key,
+          implode(', ', array_keys($protocols))));
+    }
+  }
+
+}
diff --git a/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
--- a/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
+++ b/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
@@ -15,4 +15,16 @@
     return pht('All Harbormaster APIs are new and subject to change.');
   }
 
+  protected function returnArtifactList(array $artifacts) {
+    $list = array();
+
+    foreach ($artifacts as $artifact) {
+      $list[] = array(
+        'phid' => $artifact->getPHID(),
+      );
+    }
+
+    return $list;
+  }
+
 }
diff --git a/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php
@@ -0,0 +1,129 @@
+<?php
+
+final class HarbormasterCreateArtifactConduitAPIMethod
+  extends HarbormasterConduitAPIMethod {
+
+  public function getAPIMethodName() {
+    return 'harbormaster.createartifact';
+  }
+
+  public function getMethodSummary() {
+    return pht('Create a build artifact.');
+  }
+
+  public function getMethodDescription() {
+    $types = HarbormasterArtifact::getAllArtifactTypes();
+    $types = msort($types, 'getArtifactTypeName');
+
+    $head_key = pht('Key');
+    $head_type = pht('Type');
+    $head_desc = pht('Description');
+    $head_atype = pht('Artifact Type');
+    $head_name = pht('Name');
+    $head_summary = pht('Summary');
+
+    $out = array();
+    $out[] = pht(
+      'Use this method to attach artifacts to build targets while running '.
+      'builds. Artifacts can be used to carry data through a complex build '.
+      'workflow, provide extra information to users, or store build results.');
+    $out[] = null;
+    $out[] = pht(
+      'When creating an artifact, you will choose an `artifactType` from '.
+      'this table. These types of artifacts are supported:');
+
+    $out[] = "| {$head_atype} | {$head_name} | {$head_summary} |";
+    $out[] = '|-------------|--------------|--------------|';
+    foreach ($types as $type) {
+      $type_name = $type->getArtifactTypeName();
+      $type_const = $type->getArtifactConstant();
+      $type_summary = $type->getArtifactTypeSummary();
+      $out[] = "| `{$type_const}` | **{$type_name}** | {$type_summary} |";
+    }
+
+    $out[] = null;
+    $out[] = pht(
+      'Each artifact also needs an `artifactKey`, which names the artifact. '.
+      'Finally, you will provide some `artifactData` to fill in the content '.
+      'of the artifact. The data you provide depends on what type of artifact '.
+      'you are creating.');
+
+    foreach ($types as $type) {
+      $type_name = $type->getArtifactTypeName();
+      $type_const = $type->getArtifactConstant();
+
+      $out[] = $type_name;
+      $out[] = '--------------------------';
+      $out[] = null;
+      $out[] = $type->getArtifactTypeDescription();
+      $out[] = null;
+      $out[] = pht(
+        'Create an artifact of this type by passing `%s` as the '.
+        '`artifactType`. When creating an artifact of this type, provide '.
+        'these parameters as a dictionary to `artifactData`:',
+        $type_const);
+
+      $spec = $type->getArtifactParameterSpecification();
+      $desc = $type->getArtifactParameterDescriptions();
+      $out[] = "| {$head_key} | {$head_type} | {$head_desc} |";
+      $out[] = '|-------------|--------------|--------------|';
+      foreach ($spec as $key => $key_type) {
+        $key_desc = idx($desc, $key);
+        $out[] = "| `{$key}` | //{$key_type}// | {$key_desc} |";
+      }
+
+      $example = $type->getArtifactDataExample();
+      if ($example !== null) {
+        $json = new PhutilJSON();
+        $rendered = $json->encodeFormatted($example);
+
+        $out[] = pht('For example:');
+        $out[] = '```lang=json';
+        $out[] = $rendered;
+        $out[] = '```';
+      }
+    }
+
+    return implode("\n", $out);
+  }
+
+  protected function defineParamTypes() {
+    return array(
+      'buildTargetPHID' => 'phid',
+      'artifactKey' => 'string',
+      'artifactType' => 'string',
+      'artifactData' => 'map<string, wild>',
+    );
+  }
+
+  protected function defineReturnType() {
+    return 'wild';
+  }
+
+  protected function execute(ConduitAPIRequest $request) {
+    $viewer = $request->getUser();
+
+    $build_target_phid = $request->getValue('buildTargetPHID');
+    $build_target = id(new HarbormasterBuildTargetQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($build_target_phid))
+      ->executeOne();
+    if (!$build_target) {
+      throw new Exception(
+        pht(
+          'No such build target "%s"!',
+          $build_target_phid));
+    }
+
+    $artifact = $build_target->createArtifact(
+      $viewer,
+      $request->getValue('artifactKey'),
+      $request->getValue('artifactType'),
+      $request->getValue('artifactData'));
+
+    return array(
+      'data' => $this->returnArtifactList(array($artifact)),
+    );
+  }
+
+}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
--- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
@@ -3,17 +3,11 @@
 final class HarbormasterBuildViewController
   extends HarbormasterController {
 
-  private $id;
-
-  public function willProcessRequest(array $data) {
-    $this->id = $data['id'];
-  }
-
-  public function processRequest() {
+  public function handleRequest(AphrontRequest $request) {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
-    $id = $this->id;
+    $id = $request->getURIData('id');
     $generation = $request->getInt('g');
 
     $build = id(new HarbormasterBuildQuery())
@@ -224,29 +218,51 @@
       ));
   }
 
-  private function buildArtifacts(
-    HarbormasterBuildTarget $build_target) {
-
-    $request = $this->getRequest();
-    $viewer = $request->getUser();
+  private function buildArtifacts(HarbormasterBuildTarget $build_target) {
+    $viewer = $this->getViewer();
 
     $artifacts = id(new HarbormasterBuildArtifactQuery())
       ->setViewer($viewer)
       ->withBuildTargetPHIDs(array($build_target->getPHID()))
       ->execute();
 
-    $list = id(new PHUIObjectItemListView())
-      ->setNoDataString(pht('This target has no associated artifacts.'))
-      ->setFlush(true);
+    $artifacts = msort($artifacts, 'getArtifactKey');
 
+    $rows = array();
     foreach ($artifacts as $artifact) {
-      $item = $artifact->getObjectItemView($viewer);
-      if ($item !== null) {
-        $list->addItem($item);
+      $impl = $artifact->getArtifactImplementation();
+
+      if ($impl) {
+        $summary = $impl->renderArtifactSummary($viewer);
+        $type_name = $impl->getArtifactTypeName();
+      } else {
+        $summary = pht('<Unknown Artifact Type>');
+        $type_name = $artifact->getType();
       }
+
+      $rows[] = array(
+        $artifact->getArtifactKey(),
+        $type_name,
+        $summary,
+      );
     }
 
-    return $list;
+    $table = id(new AphrontTableView($rows))
+      ->setNoDataString(pht('This target has no associated artifacts.'))
+      ->setHeaders(
+        array(
+          pht('Key'),
+          pht('Type'),
+          pht('Summary'),
+        ))
+      ->setColumnClasses(
+        array(
+          'pri',
+          '',
+          'wide',
+        ));
+
+    return $table;
   }
 
   private function buildLog(
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
--- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
@@ -483,7 +483,7 @@
       ->execute();
 
     foreach ($artifacts as $artifact) {
-      $artifact->release();
+      $artifact->releaseArtifact();
     }
 
   }
diff --git a/src/applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php b/src/applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php
@@ -0,0 +1,35 @@
+<?php
+
+final class HarbormasterBuildArtifactPHIDType extends PhabricatorPHIDType {
+
+  const TYPECONST = 'HMBA';
+
+  public function getTypeName() {
+    return pht('Build Artifact');
+  }
+
+  public function newObject() {
+    return new HarbormasterBuildArtifact();
+  }
+
+  protected function buildQueryForObjects(
+    PhabricatorObjectQuery $query,
+    array $phids) {
+
+    return id(new HarbormasterBuildArtifactQuery())
+      ->withPHIDs($phids);
+  }
+
+  public function loadHandles(
+    PhabricatorHandleQuery $query,
+    array $handles,
+    array $objects) {
+
+    foreach ($handles as $phid => $handle) {
+      $artifact = $objects[$phid];
+      $artifact_id = $artifact->getID();
+      $handle->setName(pht('Build Artifact %d', $artifact_id));
+    }
+  }
+
+}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php
--- a/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php
@@ -6,7 +6,7 @@
   private $ids;
   private $buildTargetPHIDs;
   private $artifactTypes;
-  private $artifactKeys;
+  private $artifactIndexes;
   private $keyBuildPHID;
   private $keyBuildGeneration;
 
@@ -25,29 +25,17 @@
     return $this;
   }
 
-  public function withArtifactKeys(
-    $build_phid,
-    $build_gen,
-    array $artifact_keys) {
-    $this->keyBuildPHID = $build_phid;
-    $this->keyBuildGeneration = $build_gen;
-    $this->artifactKeys = $artifact_keys;
+  public function withArtifactIndexes(array $artifact_indexes) {
+    $this->artifactIndexes = $artifact_indexes;
     return $this;
   }
 
+  public function newResultObject() {
+    return new HarbormasterBuildArtifact();
+  }
+
   protected function loadPage() {
-    $table = new HarbormasterBuildArtifact();
-    $conn_r = $table->establishConnection('r');
-
-    $data = queryfx_all(
-      $conn_r,
-      'SELECT * FROM %T %Q %Q %Q',
-      $table->getTableName(),
-      $this->buildWhereClause($conn_r),
-      $this->buildOrderClause($conn_r),
-      $this->buildLimitClause($conn_r));
-
-    return $table->loadAllFromArray($data);
+    return $this->loadStandardPage($this->newResultObject());
   }
 
   protected function willFilterPage(array $page) {
@@ -75,46 +63,38 @@
     return $page;
   }
 
-  protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
-    $where = array();
+  protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+    $where = parent::buildWhereClauseParts($conn);
 
-    if ($this->ids) {
+    if ($this->ids !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'id IN (%Ld)',
         $this->ids);
     }
 
-    if ($this->buildTargetPHIDs) {
+    if ($this->buildTargetPHIDs !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'buildTargetPHID IN (%Ls)',
         $this->buildTargetPHIDs);
     }
 
-    if ($this->artifactTypes) {
+    if ($this->artifactTypes !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'artifactType in (%Ls)',
         $this->artifactTypes);
     }
 
-    if ($this->artifactKeys) {
-      $indexes = array();
-      foreach ($this->artifactKeys as $key) {
-        $indexes[] = PhabricatorHash::digestForIndex(
-          $this->keyBuildPHID.$this->keyBuildGeneration.$key);
-      }
-
+    if ($this->artifactIndexes !== null) {
       $where[] = qsprintf(
-        $conn_r,
+        $conn,
         'artifactIndex IN (%Ls)',
-        $indexes);
+        $this->artifactIndexes);
     }
 
-    $where[] = $this->buildPagingClause($conn_r);
-
-    return $this->formatWhereClause($where);
+    return $where;
   }
 
   public function getQueryApplicationClass() {
diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
@@ -43,9 +43,9 @@
     $settings = $this->getSettings();
     $variables = $build_target->getVariables();
 
-    $artifact = $build->loadArtifact($settings['hostartifact']);
-
-    $lease = $artifact->loadDrydockLease();
+    $artifact = $build_target->loadArtifact($settings['hostartifact']);
+    $impl = $artifact->getArtifactImplementation();
+    $lease = $impl->loadArtifactLease();
 
     $this->platform = $lease->getAttribute('platform');
 
@@ -120,9 +120,9 @@
   public function getArtifactInputs() {
     return array(
       array(
-        'name'  => pht('Run on Host'),
-        'key'   => $this->getSetting('hostartifact'),
-        'type'  => HarbormasterBuildArtifact::TYPE_HOST,
+        'name' => pht('Run on Host'),
+        'key' => $this->getSetting('hostartifact'),
+        'type' => HarbormasterHostArtifact::ARTIFACTCONST,
       ),
     );
   }
diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
@@ -36,14 +36,13 @@
     $lease->waitUntilActive();
 
     // Create the associated artifact.
-    $artifact = $build->createArtifact(
-      $build_target,
+    $artifact = $build_target->createArtifact(
+      PhabricatorUser::getOmnipotentUser(),
       $settings['name'],
-      HarbormasterBuildArtifact::TYPE_HOST);
-    $artifact->setArtifactData(array(
-      'drydock-lease' => $lease->getID(),
-    ));
-    $artifact->save();
+      HarbormasterHostArtifact::ARTIFACTCONST,
+      array(
+        'drydockLeasePHID' => $lease->getPHID(),
+      ));
   }
 
   public function getArtifactOutputs() {
@@ -51,7 +50,7 @@
       array(
         'name' => pht('Leased Host'),
         'key' => $this->getSetting('name'),
-        'type' => HarbormasterBuildArtifact::TYPE_HOST,
+        'type' => HarbormasterHostArtifact::ARTIFACTCONST,
       ),
     );
   }
diff --git a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php
@@ -29,33 +29,34 @@
 
     $settings = $this->getSettings();
     $variables = $build_target->getVariables();
+    $viewer = PhabricatorUser::getOmnipotentUser();
 
     $path = $this->mergeVariables(
       'vsprintf',
       $settings['path'],
       $variables);
 
-    $artifact = $build->loadArtifact($settings['artifact']);
-
-    $file = $artifact->loadPhabricatorFile();
+    $artifact = $build_target->loadArtifact($settings['artifact']);
+    $impl = $artifact->getArtifactImplementation();
+    $file = $impl->loadArtifactFile($viewer);
 
     $fragment = id(new PhragmentFragmentQuery())
-      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->setViewer($viewer)
       ->withPaths(array($path))
       ->executeOne();
 
     if ($fragment === null) {
       PhragmentFragment::createFromFile(
-        PhabricatorUser::getOmnipotentUser(),
+        $viewer,
         $file,
         $path,
         PhabricatorPolicies::getMostOpenPolicy(),
         PhabricatorPolicies::POLICY_USER);
     } else {
       if ($file->getMimeType() === 'application/zip') {
-        $fragment->updateFromZIP(PhabricatorUser::getOmnipotentUser(), $file);
+        $fragment->updateFromZIP($viewer, $file);
       } else {
-        $fragment->updateFromFile(PhabricatorUser::getOmnipotentUser(), $file);
+        $fragment->updateFromFile($viewer, $file);
       }
     }
   }
@@ -65,7 +66,7 @@
       array(
         'name' => pht('Publishes File'),
         'key' => $this->getSetting('artifact'),
-        'type' => HarbormasterBuildArtifact::TYPE_FILE,
+        'type' => HarbormasterFileArtifact::ARTIFACTCONST,
       ),
     );
   }
diff --git a/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php
@@ -34,8 +34,7 @@
       $settings['path'],
       $variables);
 
-    $artifact = $build->loadArtifact($settings['hostartifact']);
-
+    $artifact = $build_target->loadArtifact($settings['hostartifact']);
     $lease = $artifact->loadDrydockLease();
 
     $interface = $lease->getInterface('filesystem');
@@ -44,14 +43,13 @@
     $file = $interface->saveFile($path, $settings['name']);
 
     // Insert the artifact record.
-    $artifact = $build->createArtifact(
-      $build_target,
+    $artifact = $build_target->createArtifact(
+      PhabricatorUser::getOmnipotentUser(),
       $settings['name'],
-      HarbormasterBuildArtifact::TYPE_FILE);
-    $artifact->setArtifactData(array(
-      'filePHID' => $file->getPHID(),
-    ));
-    $artifact->save();
+      HarbormasterFileArtifact::ARTIFACTCONST,
+      array(
+        'filePHID' => $file->getPHID(),
+      ));
   }
 
   public function getArtifactInputs() {
@@ -59,7 +57,7 @@
       array(
         'name' => pht('Upload From Host'),
         'key' => $this->getSetting('hostartifact'),
-        'type' => HarbormasterBuildArtifact::TYPE_HOST,
+        'type' => HarbormasterHostArtifact::ARTIFACTCONST,
       ),
     );
   }
@@ -69,7 +67,7 @@
       array(
         'name' => pht('Uploaded File'),
         'key' => $this->getSetting('name'),
-        'type' => HarbormasterBuildArtifact::TYPE_FILE,
+        'type' => HarbormasterHostArtifact::ARTIFACTCONST,
       ),
     );
   }
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -235,36 +235,6 @@
     return $log;
   }
 
-  public function createArtifact(
-    HarbormasterBuildTarget $build_target,
-    $artifact_key,
-    $artifact_type) {
-
-    $artifact =
-      HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target);
-    $artifact->setArtifactKey(
-      $this->getPHID(),
-      $this->getBuildGeneration(),
-      $artifact_key);
-    $artifact->setArtifactType($artifact_type);
-    $artifact->save();
-    return $artifact;
-  }
-
-  public function loadArtifact($name) {
-    $artifact = id(new HarbormasterBuildArtifactQuery())
-      ->setViewer(PhabricatorUser::getOmnipotentUser())
-      ->withArtifactKeys(
-        $this->getPHID(),
-        $this->getBuildGeneration(),
-        array($name))
-      ->executeOne();
-    if ($artifact === null) {
-      throw new Exception(pht('Artifact not found!'));
-    }
-    return $artifact;
-  }
-
   public function retrieveVariablesFromBuild() {
     $results = array(
       'buildable.diff' => null,
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
@@ -10,19 +10,18 @@
   protected $artifactData = array();
 
   private $buildTarget = self::ATTACHABLE;
-
-  const TYPE_FILE = 'file';
-  const TYPE_HOST = 'host';
-  const TYPE_URI = 'uri';
+  private $artifactImplementation;
 
   public static function initializeNewBuildArtifact(
     HarbormasterBuildTarget $build_target) {
     return id(new HarbormasterBuildArtifact())
+      ->attachBuildTarget($build_target)
       ->setBuildTargetPHID($build_target->getPHID());
   }
 
   protected function getConfiguration() {
     return array(
+      self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'artifactData' => self::SERIALIZATION_JSON,
       ),
@@ -43,6 +42,11 @@
     ) + parent::getConfiguration();
   }
 
+  public function generatePHID() {
+    return PhabricatorPHID::generateNewPHID(
+      HarbormasterBuildArtifactPHIDType::TYPECONST);
+  }
+
   public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
     $this->buildTarget = $build_target;
     return $this;
@@ -52,113 +56,58 @@
     return $this->assertAttached($this->buildTarget);
   }
 
-  public function setArtifactKey($build_phid, $build_gen, $key) {
-    $this->artifactIndex =
-      PhabricatorHash::digestForIndex($build_phid.$build_gen.$key);
+  public function setArtifactKey($key) {
+    $target = $this->getBuildTarget();
+    $this->artifactIndex = self::getArtifactIndex($target, $key);
     $this->artifactKey = $key;
     return $this;
   }
 
-  public function getObjectItemView(PhabricatorUser $viewer) {
-    $data = $this->getArtifactData();
-    switch ($this->getArtifactType()) {
-      case self::TYPE_FILE:
-        $handle = id(new PhabricatorHandleQuery())
-          ->setViewer($viewer)
-          ->withPHIDs($data)
-          ->executeOne();
-
-        return id(new PHUIObjectItemView())
-          ->setObjectName(pht('File'))
-          ->setHeader($handle->getFullName())
-          ->setHref($handle->getURI());
-      case self::TYPE_HOST:
-        $leases = id(new DrydockLeaseQuery())
-          ->setViewer($viewer)
-          ->withIDs(array($data['drydock-lease']))
-          ->execute();
-        $lease = idx($leases, $data['drydock-lease']);
-
-        return id(new PHUIObjectItemView())
-          ->setObjectName(pht('Drydock Lease'))
-          ->setHeader($lease->getID())
-          ->setHref('/drydock/lease/'.$lease->getID());
-      case self::TYPE_URI:
-        return id(new PHUIObjectItemView())
-          ->setObjectName($data['name'])
-          ->setHeader($data['uri'])
-          ->setHref($data['uri']);
-      default:
-        return null;
-    }
-  }
-
-  public function loadDrydockLease() {
-    if ($this->getArtifactType() !== self::TYPE_HOST) {
-      throw new Exception(
-        pht(
-          '`%s` may only be called on host artifacts.',
-          __FUNCTION__));
-    }
+  public static function getArtifactIndex(
+    HarbormasterBuildTarget $target,
+    $artifact_key) {
 
-    $data = $this->getArtifactData();
+    $build = $target->getBuild();
 
-    // FIXME: Is there a better way of doing this?
-    // TODO: Policy stuff, etc.
-    $lease = id(new DrydockLease())->load(
-      $data['drydock-lease']);
-    if ($lease === null) {
-      throw new Exception(pht('Associated Drydock lease not found!'));
-    }
-    $resource = id(new DrydockResource())->load(
-      $lease->getResourceID());
-    if ($resource === null) {
-      throw new Exception(pht('Associated Drydock resource not found!'));
-    }
-    $lease->attachResource($resource);
+    $parts = array(
+      $build->getPHID(),
+      $target->getBuildGeneration(),
+      $artifact_key,
+    );
+    $parts = implode("\0", $parts);
 
-    return $lease;
+    return PhabricatorHash::digestForIndex($parts);
   }
 
-  public function loadPhabricatorFile() {
-    if ($this->getArtifactType() !== self::TYPE_FILE) {
-      throw new Exception(
-        pht(
-          '`%s` may only be called on file artifacts.',
-          __FUNCTION__));
+  public function releaseArtifact() {
+    $impl = $this->getArtifactImplementation();
+
+    if ($impl) {
+      $impl->releaseArtifact(PhabricatorUser::getOmnipotentUser());
     }
 
-    $data = $this->getArtifactData();
+    return null;
+  }
 
-    // The data for TYPE_FILE is an array with a single PHID in it.
-    $phid = $data['filePHID'];
+  public function getArtifactImplementation() {
+    if ($this->artifactImplementation === null) {
+      $type = $this->getArtifactType();
+      $impl = HarbormasterArtifact::getArtifactType($type);
+      if (!$impl) {
+        return null;
+      }
 
-    $file = id(new PhabricatorFileQuery())
-      ->setViewer(PhabricatorUser::getOmnipotentUser())
-      ->withPHIDs(array($phid))
-      ->executeOne();
-    if ($file === null) {
-      throw new Exception(pht('Associated file not found!'));
+      $impl = clone $impl;
+      $impl->setBuildArtifact($this);
+      $this->artifactImplementation = $impl;
     }
-    return $file;
-  }
 
-  public function release() {
-    switch ($this->getArtifactType()) {
-      case self::TYPE_HOST:
-        $this->releaseDrydockLease();
-        break;
-    }
+    return $this->artifactImplementation;
   }
 
-  public function releaseDrydockLease() {
-    $lease = $this->loadDrydockLease();
-    $resource = $lease->getResource();
-    $blueprint = $resource->getBlueprint();
 
-    if ($lease->isActive()) {
-      $blueprint->releaseLease($resource, $lease);
-    }
+  public function getProperty($key, $default = null) {
+    return idx($this->artifactData, $key, $default);
   }
 
 
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
@@ -201,6 +201,54 @@
     );
   }
 
+  public function createArtifact(
+    PhabricatorUser $actor,
+    $artifact_key,
+    $artifact_type,
+    array $artifact_data) {
+
+    $impl = HarbormasterArtifact::getArtifactType($artifact_type);
+    if (!$impl) {
+      throw new Exception(
+        pht(
+          'There is no implementation available for artifacts of type "%s".',
+          $artifact_type));
+    }
+
+    $impl->validateArtifactData($artifact_data);
+
+    $artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($this)
+      ->setArtifactKey($artifact_key)
+      ->setArtifactType($artifact_type)
+      ->setArtifactData($artifact_data);
+
+    $impl = $artifact->getArtifactImplementation();
+    $impl->willCreateArtifact($actor);
+
+    return $artifact->save();
+  }
+
+  public function loadArtifact($artifact_key) {
+    $indexes = array();
+
+    $indexes[] = HarbormasterBuildArtifact::getArtifactIndex(
+      $this,
+      $artifact_key);
+
+    $artifact = id(new HarbormasterBuildArtifactQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withArtifactIndexes($indexes)
+      ->executeOne();
+    if ($artifact === null) {
+      throw new Exception(
+        pht(
+          'Artifact "%s" not found!',
+          $artifact_key));
+    }
+
+    return $artifact;
+  }
+
 
 /* -(  Status  )------------------------------------------------------------- */