diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript
index e9a4e66b9c..97cf0166b1 100644
--- a/src/mongo/db/pipeline/SConscript
+++ b/src/mongo/db/pipeline/SConscript
@@ -323,6 +323,7 @@ pipelineEnv.Library(
         'document_source_sort.cpp',
         'document_source_sort_by_count.cpp',
         'document_source_tee_consumer.cpp',
+        'document_source_union_with.cpp',
         'document_source_unwind.cpp',
         'pipeline.cpp',
         'semantic_analysis.cpp',
diff --git a/src/mongo/db/pipeline/document_source_union_with.cpp b/src/mongo/db/pipeline/document_source_union_with.cpp
new file mode 100644
index 0000000000..591db2bbc9
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_union_with.cpp
@@ -0,0 +1,192 @@
+/**
+ *    Copyright (C) 2019-present MongoDB, Inc.
+ *
+ *    This program is free software: you can redistribute it and/or modify
+ *    it under the terms of the Server Side Public License, version 1,
+ *    as published by MongoDB, Inc.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    Server Side Public License for more details.
+ *
+ *    You should have received a copy of the Server Side Public License
+ *    along with this program. If not, see
+ *    <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ *    As a special exception, the copyright holders give permission to link the
+ *    code of portions of this program with the OpenSSL library under certain
+ *    conditions as described in each individual source file and distribute
+ *    linked combinations including the program with the OpenSSL library. You
+ *    must comply with the Server Side Public License in all respects for
+ *    all of the code used other than as permitted herein. If you modify file(s)
+ *    with this exception, you may extend this exception to your version of the
+ *    file(s), but you are not obligated to do so. If you do not wish to do so,
+ *    delete this exception statement from your version. If you delete this
+ *    exception statement from all source files in the program, then also delete
+ *    it in the license file.
+ */
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/pipeline/document_source_union_with.h"
+
+namespace mongo {
+
+REGISTER_DOCUMENT_SOURCE(unionWith,
+                         DocumentSourceUnionWith::LiteParsed::parse,
+                         DocumentSourceUnionWith::createFromBson);
+
+std::unique_ptr<DocumentSourceUnionWith::LiteParsed> DocumentSourceUnionWith::LiteParsed::parse(
+    const AggregationRequest& request, const BSONElement& spec) {
+    uassert(ErrorCodes::FailedToParse,
+            str::stream()
+                << "the $unionWith stage specification must be an object or string, but found "
+                << typeName(spec.type()),
+            spec.type() == BSONType::Object || spec.type() == BSONType::String);
+
+    NamespaceString unionNss;
+    stdx::unordered_set<NamespaceString> foreignNssSet;
+    boost::optional<LiteParsedPipeline> liteParsedPipeline;
+    if (spec.type() == BSONType::String) {
+        unionNss = NamespaceString(request.getNamespaceString().db(), spec.valueStringData());
+    } else {
+        unionNss =
+            NamespaceString(request.getNamespaceString().db(), spec["coll"].valueStringData());
+
+        // Recursively lite parse the nested pipeline, if one exists.
+        if (auto pipelineElem = spec["pipeline"]) {
+            auto pipeline =
+                uassertStatusOK(AggregationRequest::parsePipelineFromBSON(pipelineElem));
+            AggregationRequest foreignAggReq(unionNss, std::move(pipeline));
+            liteParsedPipeline = LiteParsedPipeline(foreignAggReq);
+
+            auto pipelineInvolvedNamespaces = liteParsedPipeline->getInvolvedNamespaces();
+            foreignNssSet.insert(pipelineInvolvedNamespaces.begin(),
+                                 pipelineInvolvedNamespaces.end());
+        }
+    }
+
+    foreignNssSet.insert(unionNss);
+
+    return std::make_unique<DocumentSourceUnionWith::LiteParsed>(
+        std::move(unionNss), std::move(foreignNssSet), std::move(liteParsedPipeline));
+}
+
+DocumentSourceUnionWith::DocumentSourceUnionWith(
+    const boost::intrusive_ptr<ExpressionContext>& expCtx,
+    NamespaceString unionNss,
+    std::vector<BSONObj> pipeline)
+    : DocumentSource(kStageName, expCtx),
+      _unionNss(std::move(unionNss)),
+      _rawPipeline(std::move(pipeline)) {
+
+    // Copy the ExpressionContext of the base aggregation, using the inner namespace instead.
+    _unionExpCtx = expCtx->copyWith(_unionNss);
+
+    // TODO SERVER-XXXX: This can't happen here in a sharded cluster, since it attaches a
+    // non-serializable $cursor stage.
+    _pipeline = pExpCtx->mongoProcessInterface->makePipeline(_rawPipeline, _unionExpCtx);
+}
+
+boost::intrusive_ptr<DocumentSource> DocumentSourceUnionWith::createFromBson(
+    BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) {
+    uassert(ErrorCodes::FailedToParse,
+            str::stream()
+                << "the $unionWith stage specification must be an object or string, but found "
+                << typeName(elem.type()),
+            elem.type() == BSONType::Object || elem.type() == BSONType::String);
+
+    NamespaceString unionNss;
+    std::vector<BSONObj> pipeline;
+    if (elem.type() == BSONType::String) {
+        unionNss = NamespaceString(expCtx->ns.db().toString(), elem.valueStringData());
+    } else {
+        for (auto&& arg : elem.embeddedObject()) {
+            auto fieldName = arg.fieldNameStringData();
+            if (fieldName == "coll")
+                unionNss = NamespaceString(expCtx->ns.db().toString(), arg.valueStringData());
+            else if (fieldName == "pipeline") {
+                pipeline = uassertStatusOK(AggregationRequest::parsePipelineFromBSON(arg));
+            } else
+                uasserted(ErrorCodes::FailedToParse,
+                          str::stream()
+                              << "Unknown argument given to $unionWith stage: " << fieldName);
+        }
+    }
+    return make_intrusive<DocumentSourceUnionWith>(
+        expCtx, std::move(unionNss), std::move(pipeline));
+}
+
+DocumentSource::GetNextResult DocumentSourceUnionWith::doGetNext() {
+    if (!_sourceExhausted) {
+        auto nextInput = pSource->getNext();
+        if (!nextInput.isEOF()) {
+            return nextInput;
+        }
+        _sourceExhausted = true;
+        // Drop down to iterate the sub-pipe
+    }
+
+    if (_pipeline) {
+        auto res = _pipeline->getNext();
+        if (res)
+            return std::move(*res);
+    }
+
+    return GetNextResult::makeEOF();
+}
+
+DocumentSource::GetModPathsReturn DocumentSourceUnionWith::getModifiedPaths() const {
+    return {GetModPathsReturn::Type::kAllPaths, {}, {}};
+}
+
+void DocumentSourceUnionWith::doDispose() {
+    if (_pipeline) {
+        _pipeline->dispose(pExpCtx->opCtx);
+        _pipeline.reset();
+    }
+}
+
+void DocumentSourceUnionWith::serializeToArray(
+    std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const {
+
+    Document doc = DOC("coll" << _unionNss.coll());
+    array.push_back(Value(doc));
+}
+
+DepsTracker::State DocumentSourceUnionWith::getDependencies(DepsTracker* deps) const {
+    return DepsTracker::State::SEE_NEXT;
+}
+
+void DocumentSourceUnionWith::detachFromOperationContext() {
+    if (_pipeline) {
+        // We have a pipeline we're going to be executing across multiple calls to getNext(), so we
+        // use Pipeline::detachFromOperationContext() to take care of updating
+        // '_unionExpCtx->opCtx'.
+        _pipeline->detachFromOperationContext();
+    } else if (_unionExpCtx) {
+        _unionExpCtx->opCtx = nullptr;
+    }
+}
+
+void DocumentSourceUnionWith::reattachToOperationContext(OperationContext* opCtx) {
+    if (_pipeline) {
+        // We have a pipeline we're going to be executing across multiple calls to getNext(), so we
+        // use Pipeline::reattachToOperationContext() to take care of updating
+        // '_unionExpCtx->opCtx'.
+        _pipeline->reattachToOperationContext(opCtx);
+        invariant(_unionExpCtx->opCtx == opCtx);
+    } else if (_unionExpCtx) {
+        _unionExpCtx->opCtx = opCtx;
+    }
+}
+
+void DocumentSourceUnionWith::addInvolvedCollections(
+    stdx::unordered_set<NamespaceString>* collectionNames) const {
+    collectionNames->insert(_unionNss);
+    for (auto&& stage : _pipeline->getSources()) {
+        stage->addInvolvedCollections(collectionNames);
+    }
+}
+
+}  // namespace mongo
diff --git a/src/mongo/db/pipeline/document_source_union_with.h b/src/mongo/db/pipeline/document_source_union_with.h
new file mode 100644
index 0000000000..7a6bdbcc32
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_union_with.h
@@ -0,0 +1,138 @@
+/**
+ *    Copyright (C) 2019-present MongoDB, Inc.
+ *
+ *    This program is free software: you can redistribute it and/or modify
+ *    it under the terms of the Server Side Public License, version 1,
+ *    as published by MongoDB, Inc.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    Server Side Public License for more details.
+ *
+ *    You should have received a copy of the Server Side Public License
+ *    along with this program. If not, see
+ *    <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ *    As a special exception, the copyright holders give permission to link the
+ *    code of portions of this program with the OpenSSL library under certain
+ *    conditions as described in each individual source file and distribute
+ *    linked combinations including the program with the OpenSSL library. You
+ *    must comply with the Server Side Public License in all respects for
+ *    all of the code used other than as permitted herein. If you modify file(s)
+ *    with this exception, you may extend this exception to your version of the
+ *    file(s), but you are not obligated to do so. If you do not wish to do so,
+ *    delete this exception statement from your version. If you delete this
+ *    exception statement from all source files in the program, then also delete
+ *    it in the license file.
+ */
+
+#pragma once
+
+#include <boost/optional.hpp>
+
+#include "mongo/db/pipeline/document_source.h"
+#include "mongo/db/pipeline/lite_parsed_pipeline.h"
+
+namespace mongo {
+
+class DocumentSourceUnionWith final : public DocumentSource {
+public:
+    static constexpr StringData kStageName = "$unionWith"_sd;
+
+    static boost::intrusive_ptr<DocumentSource> createFromBson(
+        BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx);
+
+    class LiteParsed final : public LiteParsedDocumentSource {
+    public:
+        static std::unique_ptr<LiteParsed> parse(const AggregationRequest& request,
+                                                 const BSONElement& spec);
+
+        LiteParsed(NamespaceString withNss,
+                   stdx::unordered_set<NamespaceString> foreignNssSet,
+                   boost::optional<LiteParsedPipeline> liteParsedPipeline)
+            : _withNss{std::move(withNss)},
+              _foreignNssSet(std::move(foreignNssSet)),
+              _liteParsedPipeline(std::move(liteParsedPipeline)) {}
+
+        stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final {
+            return {_foreignNssSet};
+        }
+
+        PrivilegeVector requiredPrivileges(bool isMongos) const final {
+            return PrivilegeVector{};
+        }
+
+        bool allowShardedForeignCollection(NamespaceString nss) const override final {
+            return true;
+        }
+
+        bool allowedToPassthroughFromMongos() const {
+            return true;
+        }
+
+    private:
+        const NamespaceString _withNss;
+        const stdx::unordered_set<NamespaceString> _foreignNssSet;
+        const boost::optional<LiteParsedPipeline> _liteParsedPipeline;
+    };
+
+    DocumentSourceUnionWith(const boost::intrusive_ptr<ExpressionContext>& expCtx,
+                            NamespaceString unionNss,
+                            std::vector<BSONObj> pipeline);
+
+    const char* getSourceName() const final {
+        return kStageName.rawData();
+    }
+
+    void serializeToArray(
+        std::vector<Value>& array,
+        boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final;
+
+    GetModPathsReturn getModifiedPaths() const final;
+
+    StageConstraints constraints(Pipeline::SplitState) const final {
+        return StageConstraints(StreamType::kStreaming,
+                                PositionRequirement::kNone,
+                                HostTypeRequirement::kNone,
+                                DiskUseRequirement::kNoDiskUse,
+                                FacetRequirement::kAllowed,
+                                TransactionRequirement::kNotAllowed,
+                                LookupRequirement::kNotAllowed);
+    }
+
+    DepsTracker::State getDependencies(DepsTracker* deps) const final;
+
+    boost::optional<DistributedPlanLogic> distributedPlanLogic() final {
+        // {shardsStage, mergingStage, sortPattern}
+        return DistributedPlanLogic{nullptr, this, boost::none};
+    }
+
+    void addInvolvedCollections(stdx::unordered_set<NamespaceString>* collectionNames) const final;
+
+    void detachFromOperationContext() final;
+
+    void reattachToOperationContext(OperationContext* opCtx) final;
+
+protected:
+    GetNextResult doGetNext() final;
+    void doDispose() final;
+
+private:
+    /**
+     * Should not be called; use serializeToArray instead.
+     */
+    Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final {
+        MONGO_UNREACHABLE;
+    }
+
+    boost::intrusive_ptr<ExpressionContext> _unionExpCtx;
+
+    NamespaceString _unionNss;
+
+    std::unique_ptr<Pipeline, PipelineDeleter> _pipeline;
+    std::vector<BSONObj> _rawPipeline;
+    bool _sourceExhausted = false;
+};
+
+}  // namespace mongo
