diff --git a/src/context/Result.cpp b/src/context/Result.cpp
index 702a2c9128981bc92de1e9fe15cfc52db892dbf7..0be5c03e6920a8bed32929c6c7d339e7a2fe71d8 100644
--- a/src/context/Result.cpp
+++ b/src/context/Result.cpp
@@ -10,7 +10,8 @@ namespace nebula {
 namespace graph {
 
 const Result& Result::EmptyResult() {
-    static Result kEmptyResult = ResultBuilder().iter(Iterator::Kind::kDefault).finish();
+    static Result kEmptyResult =
+        ResultBuilder().value(Value()).iter(Iterator::Kind::kDefault).finish();
     return kEmptyResult;
 }
 
diff --git a/src/executor/CMakeLists.txt b/src/executor/CMakeLists.txt
index c4ec39f0699ad3ce122a5267e269ec304047b6c2..06133dba7189303ae33f254247b20a36c7f2c91c 100644
--- a/src/executor/CMakeLists.txt
+++ b/src/executor/CMakeLists.txt
@@ -28,6 +28,8 @@ nebula_add_library(
     query/DataCollectExecutor.cpp
     query/DataJoinExecutor.cpp
     query/IndexScanExecutor.cpp
+    algo/ConjunctPathExecutor.cpp
+    algo/BFSShortestPathExecutor.cpp
     admin/SwitchSpaceExecutor.cpp
     admin/CreateUserExecutor.cpp
     admin/DropUserExecutor.cpp
diff --git a/src/executor/Executor.cpp b/src/executor/Executor.cpp
index 0c8882c43033b6f89112e79fa2538fe6d5718fcd..4e3fe652e18a5530182a990c2fe1ea1f1e2b7f12 100644
--- a/src/executor/Executor.cpp
+++ b/src/executor/Executor.cpp
@@ -34,6 +34,8 @@
 #include "executor/admin/SubmitJobExecutor.h"
 #include "executor/admin/SwitchSpaceExecutor.h"
 #include "executor/admin/UpdateUserExecutor.h"
+#include "executor/algo/BFSShortestPathExecutor.h"
+#include "executor/algo/ConjunctPathExecutor.h"
 #include "executor/logic/LoopExecutor.h"
 #include "executor/logic/PassThroughExecutor.h"
 #include "executor/logic/SelectExecutor.h"
@@ -365,6 +367,12 @@ Executor *Executor::makeExecutor(QueryContext *qctx, const PlanNode *node) {
         case PlanNode::Kind::kShowCollation: {
             return pool->add(new ShowCollationExecutor(node, qctx));
         }
+        case PlanNode::Kind::kBFSShortest: {
+            return pool->add(new BFSShortestPathExecutor(node, qctx));
+        }
+        case PlanNode::Kind::kConjunctPath: {
+            return pool->add(new ConjunctPathExecutor(node, qctx));
+        }
         case PlanNode::Kind::kUnknown: {
             break;
         }
diff --git a/src/executor/algo/BFSShortestPathExecutor.cpp b/src/executor/algo/BFSShortestPathExecutor.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..45975574dcd61bd23be02205517d88482032e243
--- /dev/null
+++ b/src/executor/algo/BFSShortestPathExecutor.cpp
@@ -0,0 +1,53 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include "executor/algo/BFSShortestPathExecutor.h"
+
+#include "planner/Algo.h"
+
+namespace nebula {
+namespace graph {
+folly::Future<Status> BFSShortestPathExecutor::execute() {
+    SCOPED_TIMER(&execTime_);
+    auto* bfs = asNode<BFSShortestPath>(node());
+    auto iter = ectx_->getResult(bfs->inputVar()).iter();
+    VLOG(1) << "current: " << node()->outputVar();
+    VLOG(1) << "input: " << bfs->inputVar();
+    DCHECK(!!iter);
+
+    DataSet ds;
+    ds.colNames = node()->colNames();
+    std::multimap<Value, Value> interim;
+
+    for (; iter->valid(); iter->next()) {
+        auto edgeVal = iter->getEdge();
+        if (!edgeVal.isEdge()) {
+            continue;
+        }
+        auto& edge = edgeVal.getEdge();
+        auto visited = visited_.find(edge.dst) != visited_.end();
+        if (visited) {
+            continue;
+        }
+
+        // save the starts.
+        visited_.emplace(edge.src);
+        VLOG(1) << "dst: " << edge.dst << " edge: " << edge;
+        interim.emplace(edge.dst, std::move(edgeVal));
+    }
+    for (auto& kv : interim) {
+        auto dst = std::move(kv.first);
+        auto edge = std::move(kv.second);
+        Row row;
+        row.values.emplace_back(dst);
+        row.values.emplace_back(std::move(edge));
+        ds.rows.emplace_back(std::move(row));
+        visited_.emplace(dst);
+    }
+    return finish(ResultBuilder().value(Value(std::move(ds))).finish());
+}
+}  // namespace graph
+}  // namespace nebula
diff --git a/src/executor/algo/BFSShortestPathExecutor.h b/src/executor/algo/BFSShortestPathExecutor.h
new file mode 100644
index 0000000000000000000000000000000000000000..cd06777dccd1ef8643d8b3271ea14c4db5a754d7
--- /dev/null
+++ b/src/executor/algo/BFSShortestPathExecutor.h
@@ -0,0 +1,26 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#ifndef EXECUTOR_QUERY_BFSSHORTESTPATHEXECUTOR_H_
+#define EXECUTOR_QUERY_BFSSHORTESTPATHEXECUTOR_H_
+
+#include "executor/Executor.h"
+
+namespace nebula {
+namespace graph {
+class BFSShortestPathExecutor final : public Executor {
+public:
+    BFSShortestPathExecutor(const PlanNode* node, QueryContext* qctx)
+        : Executor("BFSShortestPath", node, qctx) {}
+
+    folly::Future<Status> execute() override;
+
+private:
+    std::unordered_set<Value>               visited_;
+};
+}  // namespace graph
+}  // namespace nebula
+#endif
diff --git a/src/executor/algo/ConjunctPathExecutor.cpp b/src/executor/algo/ConjunctPathExecutor.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..534acb3fee16f6480faf6c7d0e36bd96c7b5956d
--- /dev/null
+++ b/src/executor/algo/ConjunctPathExecutor.cpp
@@ -0,0 +1,235 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include "executor/algo/ConjunctPathExecutor.h"
+
+#include "planner/Algo.h"
+
+namespace nebula {
+namespace graph {
+folly::Future<Status> ConjunctPathExecutor::execute() {
+    SCOPED_TIMER(&execTime_);
+    auto* conjunct = asNode<ConjunctPath>(node());
+    switch (conjunct->pathKind()) {
+        case ConjunctPath::PathKind::kBiBFS:
+            return bfsShortestPath();
+        default:
+            LOG(FATAL) << "Not implement.";
+    }
+}
+
+folly::Future<Status> ConjunctPathExecutor::bfsShortestPath() {
+    auto* conjunct = asNode<ConjunctPath>(node());
+    auto lIter = ectx_->getResult(conjunct->leftInputVar()).iter();
+    const auto& rHist = ectx_->getHistory(conjunct->rightInputVar());
+    VLOG(1) << "current: " << node()->outputVar();
+    VLOG(1) << "left input: " << conjunct->leftInputVar()
+            << " right input: " << conjunct->rightInputVar();
+    DCHECK(!!lIter);
+
+    DataSet ds;
+    ds.colNames = conjunct->colNames();
+
+    VLOG(1) << "forward, size: " << forward_.size();
+    VLOG(1) << "backward, size: " << backward_.size();
+    forward_.emplace_back();
+    for (; lIter->valid(); lIter->next()) {
+        auto& dst = lIter->getColumn(kVid);
+        auto& edge = lIter->getColumn("edge");
+        VLOG(1) << "dst: " << dst << " edge: " << edge;
+        if (!edge.isEdge()) {
+            forward_.back().emplace(Value(dst), nullptr);
+        } else {
+            forward_.back().emplace(Value(dst), &edge.getEdge());
+        }
+    }
+
+    bool isLatest = false;
+    if (rHist.size() >= 2) {
+        auto previous = rHist[rHist.size() - 2].iter();
+        VLOG(1) << "Find odd length path.";
+        auto rows = findBfsShortestPath(previous.get(), isLatest, forward_.back());
+        if (!rows.empty()) {
+            VLOG(1) << "Meet odd length path.";
+            ds.rows = std::move(rows);
+            return finish(ResultBuilder().value(Value(std::move(ds))).finish());
+        }
+    }
+
+    auto latest = rHist.back().iter();
+    isLatest = true;
+    backward_.emplace_back();
+    VLOG(1) << "Find even length path.";
+    auto rows = findBfsShortestPath(latest.get(), isLatest, forward_.back());
+    if (!rows.empty()) {
+        VLOG(1) << "Meet even length path.";
+        ds.rows = std::move(rows);
+    }
+    return finish(ResultBuilder().value(Value(std::move(ds))).finish());
+}
+
+std::vector<Row> ConjunctPathExecutor::findBfsShortestPath(
+    Iterator* iter,
+    bool isLatest,
+    std::multimap<Value, const Edge*>& table) {
+    std::unordered_set<Value> meets;
+    for (; iter->valid(); iter->next()) {
+        auto& dst = iter->getColumn(kVid);
+        if (isLatest) {
+            auto& edge = iter->getColumn("edge");
+            VLOG(1) << "dst: " << dst << " edge: " << edge;
+            if (!edge.isEdge()) {
+                backward_.back().emplace(dst, nullptr);
+            } else {
+                backward_.back().emplace(dst, &edge.getEdge());
+            }
+        }
+        if (table.find(dst) != table.end()) {
+            meets.emplace(dst);
+        }
+    }
+
+    std::vector<Row> rows;
+    if (!meets.empty()) {
+        VLOG(1) << "Build forward, size: " << forward_.size();
+        auto forwardPath = buildBfsInterimPath(meets, forward_);
+        VLOG(1) << "Build backward, size: " << backward_.size();
+        auto backwardPath = buildBfsInterimPath(meets, backward_);
+        for (auto& p : forwardPath) {
+            auto range = backwardPath.equal_range(p.first);
+            for (auto& i = range.first; i != range.second; ++i) {
+                Path result = p.second;
+                result.reverse();
+                VLOG(1) << "Forward path: " << result;
+                VLOG(1) << "Backward path: " << i->second;
+                result.append(i->second);
+                Row row;
+                row.emplace_back(std::move(result));
+                rows.emplace_back(std::move(row));
+            }
+        }
+    }
+    return rows;
+}
+
+std::multimap<Value, Path> ConjunctPathExecutor::buildBfsInterimPath(
+    std::unordered_set<Value>& meets,
+    std::vector<std::multimap<Value, const Edge*>>& hists) {
+    std::multimap<Value, Path> results;
+    for (auto& v : meets) {
+        VLOG(1) << "Meet at: " << v;
+        Path start;
+        start.src = Vertex(v.getStr(), {});
+        if (hists.empty()) {
+            // Happens at one step path situation when meet at starts
+            VLOG(1) << "Start: " << start;
+            results.emplace(v, std::move(start));
+            continue;
+        }
+        std::vector<Path> interimPaths = {std::move(start)};
+        for (auto hist = hists.rbegin(); hist < hists.rend(); ++hist) {
+            std::vector<Path> tmp;
+            for (auto& interimPath : interimPaths) {
+                Value id;
+                if (interimPath.steps.empty()) {
+                    id = interimPath.src.vid;
+                } else {
+                    id = interimPath.steps.back().dst.vid;
+                }
+                auto edges = hist->equal_range(id);
+                for (auto i = edges.first; i != edges.second; ++i) {
+                    Path p = interimPath;
+                    if (i->second != nullptr) {
+                        auto& edge = *(i->second);
+                        VLOG(1) << "Edge: " << edge;
+                        VLOG(1) << "Interim path: " << interimPath;
+                        p.steps.emplace_back(
+                            Step(Vertex(edge.src, {}), -edge.type, edge.name, edge.ranking, {}));
+                        VLOG(1) << "New semi path: " << p;
+                    }
+                    if (hist == (hists.rend() - 1)) {
+                        VLOG(1) << "emplace result: " << p.src.vid;
+                        results.emplace(p.src.vid, std::move(p));
+                    } else {
+                        tmp.emplace_back(std::move(p));
+                    }
+                }   // `edge'
+            }       // `interimPath'
+            if (hist != (hists.rend() - 1)) {
+                interimPaths = std::move(tmp);
+            }
+        }   // `hist'
+    }       // `v'
+    return results;
+}
+
+folly::Future<Status> ConjunctPathExecutor::conjunctPath() {
+    auto* conjunct = asNode<ConjunctPath>(node());
+    auto lIter = ectx_->getResult(conjunct->leftInputVar()).iter();
+    const auto& rHist = ectx_->getHistory(conjunct->rightInputVar());
+    VLOG(1) << "current: " << node()->outputVar();
+    VLOG(1) << "left input: " << conjunct->leftInputVar()
+            << " right input: " << conjunct->rightInputVar();
+    DCHECK(!!lIter);
+
+    DataSet ds;
+    ds.colNames = conjunct->colNames();
+
+    std::multimap<Value, const Path*> table;
+    for (; lIter->valid(); lIter->next()) {
+        auto& dst = lIter->getColumn(kVid);
+        auto& path = lIter->getColumn("path");
+        if (path.isPath() && !path.getPath().steps.empty()) {
+            VLOG(1) << "Forward dst: " << dst;
+            table.emplace(dst, &path.getPath());
+        }
+    }
+
+    if (rHist.size() >= 2) {
+        auto previous = rHist[rHist.size() - 2].iter();
+        if (findPath(previous.get(), table, ds)) {
+            VLOG(1) << "Meet odd length path.";
+            return finish(ResultBuilder().value(Value(std::move(ds))).finish());
+        }
+    }
+
+    auto latest = rHist.back().iter();
+    findPath(latest.get(), table, ds);
+    return finish(ResultBuilder().value(Value(std::move(ds))).finish());
+}
+
+bool ConjunctPathExecutor::findPath(Iterator* iter,
+                                    std::multimap<Value, const Path*>& table,
+                                    DataSet& ds) {
+    bool found = false;
+    for (; iter->valid(); iter->next()) {
+        auto& dst = iter->getColumn(kVid);
+        VLOG(1) << "Backward dst: " << dst;
+        auto& path = iter->getColumn("path");
+        if (path.isPath()) {
+            auto paths = table.equal_range(dst);
+            if (paths.first != paths.second) {
+                for (auto i = paths.first; i != paths.second; ++i) {
+                    Row row;
+                    auto forward = *i->second;
+                    Path backward = path.getPath();
+                    VLOG(1) << "Forward path:" << forward;
+                    VLOG(1) << "Backward path:" << backward;
+                    backward.reverse();
+                    VLOG(1) << "Backward reverse path:" << backward;
+                    forward.append(std::move(backward));
+                    VLOG(1) << "Found path: " << forward;
+                    row.values.emplace_back(std::move(forward));
+                    ds.rows.emplace_back(std::move(row));
+                }
+                found = true;
+            }
+        }
+    }
+    return found;
+}
+}  // namespace graph
+}  // namespace nebula
diff --git a/src/executor/algo/ConjunctPathExecutor.h b/src/executor/algo/ConjunctPathExecutor.h
new file mode 100644
index 0000000000000000000000000000000000000000..0244a752535f42474e6a8aff92b518c2f9a2b5cf
--- /dev/null
+++ b/src/executor/algo/ConjunctPathExecutor.h
@@ -0,0 +1,41 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#ifndef EXECUTOR_QUERY_CONJUNCTPATHEXECUTOR_H_
+#define EXECUTOR_QUERY_CONJUNCTPATHEXECUTOR_H_
+
+#include "executor/Executor.h"
+
+namespace nebula {
+namespace graph {
+class ConjunctPathExecutor final : public Executor {
+public:
+    ConjunctPathExecutor(const PlanNode* node, QueryContext* qctx)
+        : Executor("ConjunctPathExecutor", node, qctx) {}
+
+    folly::Future<Status> execute() override;
+
+private:
+    folly::Future<Status> bfsShortestPath();
+
+    std::vector<Row> findBfsShortestPath(Iterator* iter,
+                                         bool isLatest,
+                                         std::multimap<Value, const Edge*>& table);
+
+    std::multimap<Value, Path> buildBfsInterimPath(
+        std::unordered_set<Value>& meets,
+        std::vector<std::multimap<Value, const Edge*>>& hist);
+
+    folly::Future<Status> conjunctPath();
+
+    bool findPath(Iterator* iter, std::multimap<Value, const Path*>& table, DataSet& ds);
+
+    std::vector<std::multimap<Value, const Edge*>>  forward_;
+    std::vector<std::multimap<Value, const Edge*>>  backward_;
+};
+}  // namespace graph
+}  // namespace nebula
+#endif  // EXECUTOR_QUERY_CONJUNCTPATHEXECUTOR_H_
diff --git a/src/executor/logic/LoopExecutor.cpp b/src/executor/logic/LoopExecutor.cpp
index ee29f64bc653934ff46637adb0513079262ec21d..29327cf7224df317192fdecb44a39885e6e4b092 100644
--- a/src/executor/logic/LoopExecutor.cpp
+++ b/src/executor/logic/LoopExecutor.cpp
@@ -28,8 +28,9 @@ folly::Future<Status> LoopExecutor::execute() {
     auto *loopNode = asNode<Loop>(node());
     Expression *expr = loopNode->condition();
     QueryExpressionContext ctx(ectx_);
+
     auto value = expr->eval(ctx);
-    VLOG(1) << "Loop condition: " << value;
+    VLOG(1) << "Loop condition: " << expr->toString() << " val: " << value;
     DCHECK(value.isBool());
     return finish(ResultBuilder().value(std::move(value)).iter(Iterator::Kind::kDefault).finish());
 }
diff --git a/src/executor/query/DataCollectExecutor.cpp b/src/executor/query/DataCollectExecutor.cpp
index 3f9a92e79c99a9956d46e482aca4606bea364b67..adeb88a54801407314762eea748161b76846e77c 100644
--- a/src/executor/query/DataCollectExecutor.cpp
+++ b/src/executor/query/DataCollectExecutor.cpp
@@ -37,6 +37,10 @@ folly::Future<Status> DataCollectExecutor::doCollect() {
             NG_RETURN_IF_ERROR(collectMToN(vars, dc->mToN(), dc->distinct()));
             break;
         }
+        case DataCollect::CollectKind::kBFSShortest: {
+            NG_RETURN_IF_ERROR(collectBFSShortest(vars));
+            break;
+        }
         default:
             LOG(FATAL) << "Unknown data collect type: " << static_cast<int64_t>(dc->collectKind());
     }
@@ -83,7 +87,9 @@ Status DataCollectExecutor::collectSubgraph(const std::vector<std::string>& vars
                 }
                 ds.rows.emplace_back(Row({std::move(vertices), std::move(edges)}));
             } else {
-                return Status::Error("Iterator should be kind of GetNeighborIter.");
+                std::stringstream msg;
+                msg << "Iterator should be kind of GetNeighborIter, but was: " << iter->kind();
+                return Status::Error(msg.str());
             }
         }
     }
@@ -134,7 +140,9 @@ Status DataCollectExecutor::collectMToN(const std::vector<std::string>& vars,
                     ds.rows.emplace_back(seqIter->moveRow());
                 }
             } else {
-                return Status::Error("Iterator should be kind of SequentialIter.");
+                std::stringstream msg;
+                msg << "Iterator should be kind of SequentialIter, but was: " << iter->kind();
+                return Status::Error(msg.str());
             }
             itersHolder.emplace_back(std::move(iter));
         }
@@ -142,5 +150,10 @@ Status DataCollectExecutor::collectMToN(const std::vector<std::string>& vars,
     result_.setDataSet(std::move(ds));
     return Status::OK();
 }
+
+Status DataCollectExecutor::collectBFSShortest(const std::vector<std::string>& vars) {
+    // Will rewrite this method once we implement returning the props for the path.
+    return rowBasedMove(vars);
+}
 }  // namespace graph
 }  // namespace nebula
diff --git a/src/executor/query/DataCollectExecutor.h b/src/executor/query/DataCollectExecutor.h
index be48dc90350dd0febd25306704e60f5bfbfed649..b376354ddb43c7bc3a550151c7d6c3ba31ab296f 100644
--- a/src/executor/query/DataCollectExecutor.h
+++ b/src/executor/query/DataCollectExecutor.h
@@ -27,6 +27,8 @@ private:
 
     Status collectMToN(const std::vector<std::string>& vars, StepClause::MToN* mToN, bool distinct);
 
+    Status collectBFSShortest(const std::vector<std::string>& vars);
+
     std::vector<std::string>    colNames_;
     Value                       result_;
 };
diff --git a/src/executor/test/BFSShortestTest.cpp b/src/executor/test/BFSShortestTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..af7f26be83bbf10e1a4083d43cf8dcdc5c0ae7d6
--- /dev/null
+++ b/src/executor/test/BFSShortestTest.cpp
@@ -0,0 +1,218 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include <gtest/gtest.h>
+
+#include "context/QueryContext.h"
+#include "planner/Algo.h"
+#include "executor/algo/BFSShortestPathExecutor.h"
+
+namespace nebula {
+namespace graph {
+class BFSShortestTest : public testing::Test {
+protected:
+    void SetUp() override {
+        qctx_ = std::make_unique<QueryContext>();
+        {
+            DataSet ds1;
+            ds1.colNames = {kVid,
+                            "_stats",
+                            "_edge:+edge1:_type:_dst:_rank",
+                            "_expr"};
+            {
+                Row row;
+                // _vid
+                row.values.emplace_back("1");
+                // _stats = empty
+                row.values.emplace_back(Value());
+                // edges
+                List edges;
+                for (auto j = 2; j < 4; ++j) {
+                    List edge;
+                    edge.values.emplace_back(1);
+                    edge.values.emplace_back(folly::to<std::string>(j));
+                    edge.values.emplace_back(0);
+                    edges.values.emplace_back(std::move(edge));
+                }
+                row.values.emplace_back(edges);
+                // _expr = empty
+                row.values.emplace_back(Value());
+                ds1.rows.emplace_back(std::move(row));
+            }
+            firstStepResult_ = std::move(ds1);
+
+            DataSet ds2;
+            ds2.colNames = {kVid,
+                            "_stats",
+                            "_edge:+edge1:_type:_dst:_rank",
+                            "_expr"};
+            {
+                Row row;
+                // _vid
+                row.values.emplace_back("2");
+                // _stats = empty
+                row.values.emplace_back(Value());
+                // edges
+                List edges;
+                for (auto j = 4; j < 6; ++j) {
+                    List edge;
+                    edge.values.emplace_back(1);
+                    edge.values.emplace_back(folly::to<std::string>(j));
+                    edge.values.emplace_back(0);
+                    edges.values.emplace_back(std::move(edge));
+                }
+                row.values.emplace_back(edges);
+                // _expr = empty
+                row.values.emplace_back(Value());
+                ds2.rows.emplace_back(std::move(row));
+            }
+            {
+                Row row;
+                // _vid
+                row.values.emplace_back("3");
+                // _stats = empty
+                row.values.emplace_back(Value());
+                // edges
+                List edges;
+                {
+                    List edge;
+                    edge.values.emplace_back(1);
+                    edge.values.emplace_back("1");
+                    edge.values.emplace_back(0);
+                    edges.values.emplace_back(std::move(edge));
+                }
+                {
+                    List edge;
+                    edge.values.emplace_back(1);
+                    edge.values.emplace_back("4");
+                    edge.values.emplace_back(0);
+                    edges.values.emplace_back(std::move(edge));
+                }
+
+                row.values.emplace_back(edges);
+                // _expr = empty
+                row.values.emplace_back(Value());
+                ds2.rows.emplace_back(std::move(row));
+            }
+            secondStepResult_ = std::move(ds2);
+        }
+        {
+            DataSet ds;
+            ds.colNames = {kVid,
+                            "_stats",
+                            "_tag:tag1:prop1:prop2",
+                            "_edge:+edge1:prop1:prop2:_dst:_rank",
+                            "_expr"};
+            qctx_->ectx()->setResult("empty_get_neighbors",
+                                     ResultBuilder()
+                                         .value(Value(std::move(ds)))
+                                         .iter(Iterator::Kind::kGetNeighbors)
+                                         .finish());
+        }
+    }
+
+protected:
+    std::unique_ptr<QueryContext> qctx_;
+    DataSet                       firstStepResult_;
+    DataSet                       secondStepResult_;
+};
+
+TEST_F(BFSShortestTest, BFSShortest) {
+    auto* bfs = BFSShortestPath::make(qctx_.get(), nullptr);
+    bfs->setInputVar("input");
+    bfs->setColNames({"_vid", "edge"});
+
+    auto bfsExe = std::make_unique<BFSShortestPathExecutor>(bfs, qctx_.get());
+    // Step 1
+    {
+        ResultBuilder builder;
+        List datasets;
+        datasets.values.emplace_back(std::move(firstStepResult_));
+        builder.value(std::move(datasets)).iter(Iterator::Kind::kGetNeighbors);
+        qctx_->ectx()->setResult("input", builder.finish());
+
+        auto future = bfsExe->execute();
+        auto status = std::move(future).get();
+        EXPECT_TRUE(status.ok());
+        auto& result = qctx_->ectx()->getResult(bfs->outputVar());
+
+        DataSet expected;
+        expected.colNames = {"_vid", "edge"};
+        {
+            Row row;
+            row.values = {"2", Edge("1", "2", 1, "edge1", 0, {})};
+            expected.rows.emplace_back(std::move(row));
+        }
+        {
+            Row row;
+            row.values = {"3", Edge("1", "3", 1, "edge1", 0, {})};
+            expected.rows.emplace_back(std::move(row));
+        }
+
+        std::sort(expected.rows.begin(), expected.rows.end());
+        auto resultDs = result.value().getDataSet();
+        std::sort(resultDs.rows.begin(), resultDs.rows.end());
+        EXPECT_EQ(resultDs, expected);
+        EXPECT_EQ(result.state(), Result::State::kSuccess);
+    }
+
+    // Step 2
+    {
+        ResultBuilder builder;
+        List datasets;
+        datasets.values.emplace_back(std::move(secondStepResult_));
+        builder.value(std::move(datasets)).iter(Iterator::Kind::kGetNeighbors);
+        qctx_->ectx()->setResult("input", builder.finish());
+
+        auto future = bfsExe->execute();
+        auto status = std::move(future).get();
+        EXPECT_TRUE(status.ok());
+        auto& result = qctx_->ectx()->getResult(bfs->outputVar());
+
+        DataSet expected;
+        expected.colNames = {"_vid", "edge"};
+        {
+            Row row;
+            row.values = {"4", Edge("2", "4", 1, "edge1", 0, {})};
+            expected.rows.emplace_back(std::move(row));
+        }
+        {
+            Row row;
+            row.values = {"5", Edge("2", "5", 1, "edge1", 0, {})};
+            expected.rows.emplace_back(std::move(row));
+        }
+        {
+            Row row;
+            row.values = {"4", Edge("3", "4", 1, "edge1", 0, {})};
+            expected.rows.emplace_back(std::move(row));
+        }
+
+        std::sort(expected.rows.begin(), expected.rows.end());
+        auto resultDs = result.value().getDataSet();
+        std::sort(resultDs.rows.begin(), resultDs.rows.end());
+        EXPECT_EQ(resultDs, expected);
+        EXPECT_EQ(result.state(), Result::State::kSuccess);
+    }
+}
+
+TEST_F(BFSShortestTest, EmptyInput) {
+    auto* bfs = BFSShortestPath::make(qctx_.get(), nullptr);
+    bfs->setInputVar("empty_get_neighbors");
+    bfs->setColNames({"_vid", "edge"});
+
+    auto bfsExe = std::make_unique<BFSShortestPathExecutor>(bfs, qctx_.get());
+    auto future = bfsExe->execute();
+    auto status = std::move(future).get();
+    EXPECT_TRUE(status.ok());
+    auto& result = qctx_->ectx()->getResult(bfs->outputVar());
+
+    DataSet expected;
+    expected.colNames = {"_vid", "edge"};
+    EXPECT_EQ(result.value().getDataSet(), expected);
+    EXPECT_EQ(result.state(), Result::State::kSuccess);
+}
+}  // namespace graph
+}  // namespace nebula
diff --git a/src/executor/test/CMakeLists.txt b/src/executor/test/CMakeLists.txt
index b777afa533f3342f16db38576bad3c6b3c44541d..75788f00f54e1bb28e3e30d92f8e6936b15e1379 100644
--- a/src/executor/test/CMakeLists.txt
+++ b/src/executor/test/CMakeLists.txt
@@ -48,7 +48,6 @@ SET(EXEC_QUERY_TEST_OBJS
 SET(EXEC_QUERY_TEST_LIBS
     ${THRIFT_LIBRARIES}
     gtest
-    gtest_main
     wangle
     proxygenhttpserver
     proxygenlib
@@ -58,6 +57,7 @@ nebula_add_test(
     NAME
         executor_test
     SOURCES
+        TestMain.cpp
         LogicExecutorsTest.cpp
         ProjectTest.cpp
         GetNeighborsTest.cpp
@@ -70,6 +70,8 @@ nebula_add_test(
         TopNTest.cpp
         AggregateTest.cpp
         DataJoinTest.cpp
+        BFSShortestTest.cpp
+        ConjunctPathTest.cpp
     OBJECTS
         ${EXEC_QUERY_TEST_OBJS}
     LIBRARIES
diff --git a/src/executor/test/ConjunctPathTest.cpp b/src/executor/test/ConjunctPathTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5c469fe7d1c26e28825d6e4b1c241fac9d1cd923
--- /dev/null
+++ b/src/executor/test/ConjunctPathTest.cpp
@@ -0,0 +1,361 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include <gtest/gtest.h>
+
+#include "context/QueryContext.h"
+#include "planner/Algo.h"
+#include "executor/algo/ConjunctPathExecutor.h"
+
+namespace nebula {
+namespace graph {
+class ConjunctPathTest : public testing::Test {
+protected:
+    void SetUp() override {
+        qctx_ = std::make_unique<QueryContext>();
+        {
+            // 1->2
+            // 1->3
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"1", Value::kEmpty};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("forward1", ResultBuilder().value(ds1).finish());
+
+            DataSet ds2;
+            ds2.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"2", Edge("1", "2", 1, "edge1", 0, {})};
+                ds2.rows.emplace_back(std::move(row));
+            }
+            {
+                Row row;
+                row.values = {"3", Edge("1", "3", 1, "edge1", 0, {})};
+                ds2.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("forward1", ResultBuilder().value(ds2).finish());
+        }
+        {
+            // 4
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"4", Value::kEmpty};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward1", ResultBuilder().value(ds1).finish());
+
+            DataSet ds2;
+            ds2.colNames = {kVid, "edge"};
+            qctx_->ectx()->setResult("backward1", ResultBuilder().value(ds2).finish());
+        }
+        {
+            // 2
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"2", Value::kEmpty};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward2", ResultBuilder().value(ds1).finish());
+
+            DataSet ds2;
+            ds2.colNames = {kVid, "edge"};
+            qctx_->ectx()->setResult("backward2", ResultBuilder().value(ds2).finish());
+        }
+        {
+            // 4->3
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"4", Value::kEmpty};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward3", ResultBuilder().value(ds1).finish());
+
+            DataSet ds2;
+            ds2.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"3", Edge("4", "3", -1, "edge1", 0, {})};
+                ds2.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward3", ResultBuilder().value(ds2).finish());
+        }
+        {
+            // 5->4
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"5", Value::kEmpty};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward4", ResultBuilder().value(ds1).finish());
+
+            DataSet ds2;
+            ds2.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"4", Edge("5", "4", -1, "edge1", 0, {})};
+                ds2.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward4", ResultBuilder().value(ds2).finish());
+        }
+    }
+
+protected:
+    std::unique_ptr<QueryContext> qctx_;
+};
+
+TEST_F(ConjunctPathTest, BiBFSNoPath) {
+    auto* conjunct =
+        ConjunctPath::make(qctx_.get(), nullptr, nullptr, ConjunctPath::PathKind::kBiBFS);
+    conjunct->setLeftVar("forward1");
+    conjunct->setRightVar("backward1");
+    conjunct->setColNames({"_path"});
+
+    auto conjunctExe = std::make_unique<ConjunctPathExecutor>(conjunct, qctx_.get());
+    auto future = conjunctExe->execute();
+    auto status = std::move(future).get();
+    EXPECT_TRUE(status.ok());
+    auto& result = qctx_->ectx()->getResult(conjunct->outputVar());
+
+    DataSet expected;
+    expected.colNames = {"_path"};
+    EXPECT_EQ(result.value().getDataSet(), expected);
+    EXPECT_EQ(result.state(), Result::State::kSuccess);
+}
+
+TEST_F(ConjunctPathTest, BiBFSOneStepPath) {
+    auto* conjunct =
+        ConjunctPath::make(qctx_.get(), nullptr, nullptr, ConjunctPath::PathKind::kBiBFS);
+    conjunct->setLeftVar("forward1");
+    conjunct->setRightVar("backward2");
+    conjunct->setColNames({"_path"});
+
+    auto conjunctExe = std::make_unique<ConjunctPathExecutor>(conjunct, qctx_.get());
+    auto future = conjunctExe->execute();
+    auto status = std::move(future).get();
+    EXPECT_TRUE(status.ok());
+    auto& result = qctx_->ectx()->getResult(conjunct->outputVar());
+
+    DataSet expected;
+    expected.colNames = {"_path"};
+    Row row;
+    Path path;
+    path.src = Vertex("1", {});
+    path.steps.emplace_back(Step(Vertex("2", {}), 1, "edge1", 0, {}));
+    row.values.emplace_back(std::move(path));
+    expected.rows.emplace_back(std::move(row));
+
+    EXPECT_EQ(result.value().getDataSet(), expected);
+    EXPECT_EQ(result.state(), Result::State::kSuccess);
+}
+
+TEST_F(ConjunctPathTest, BiBFSTwoStepsPath) {
+    auto* conjunct =
+        ConjunctPath::make(qctx_.get(), nullptr, nullptr, ConjunctPath::PathKind::kBiBFS);
+    conjunct->setLeftVar("forward1");
+    conjunct->setRightVar("backward3");
+    conjunct->setColNames({"_path"});
+
+    auto conjunctExe = std::make_unique<ConjunctPathExecutor>(conjunct, qctx_.get());
+    auto future = conjunctExe->execute();
+    auto status = std::move(future).get();
+    EXPECT_TRUE(status.ok());
+    auto& result = qctx_->ectx()->getResult(conjunct->outputVar());
+
+    DataSet expected;
+    expected.colNames = {"_path"};
+    Row row;
+    Path path;
+    path.src = Vertex("1", {});
+    path.steps.emplace_back(Step(Vertex("3", {}), 1, "edge1", 0, {}));
+    path.steps.emplace_back(Step(Vertex("4", {}), 1, "edge1", 0, {}));
+    row.values.emplace_back(std::move(path));
+    expected.rows.emplace_back(std::move(row));
+
+    EXPECT_EQ(result.value().getDataSet(), expected);
+    EXPECT_EQ(result.state(), Result::State::kSuccess);
+}
+
+TEST_F(ConjunctPathTest, BiBFSThreeStepsPath) {
+    auto* conjunct =
+        ConjunctPath::make(qctx_.get(), nullptr, nullptr, ConjunctPath::PathKind::kBiBFS);
+    conjunct->setLeftVar("forward1");
+    conjunct->setRightVar("backward4");
+    conjunct->setColNames({"_path"});
+
+    auto conjunctExe = std::make_unique<ConjunctPathExecutor>(conjunct, qctx_.get());
+
+    {
+        auto future = conjunctExe->execute();
+        auto status = std::move(future).get();
+        EXPECT_TRUE(status.ok());
+        auto& result = qctx_->ectx()->getResult(conjunct->outputVar());
+
+        DataSet expected;
+        expected.colNames = {"_path"};
+        EXPECT_EQ(result.value().getDataSet(), expected);
+        EXPECT_EQ(result.state(), Result::State::kSuccess);
+    }
+
+    {
+        {
+            // 2->4@0
+            // 2->4@1
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"4", Edge("2", "4", 1, "edge1", 0, {})};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            {
+                Row row;
+                row.values = {"4", Edge("2", "4", 1, "edge1", 1, {})};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("forward1", ResultBuilder().value(ds1).finish());
+        }
+        {
+            // 4->6@0
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"6", Edge("4", "6", -1, "edge1", 0, {})};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward4", ResultBuilder().value(ds1).finish());
+        }
+        auto future = conjunctExe->execute();
+        auto status = std::move(future).get();
+        EXPECT_TRUE(status.ok());
+        auto& result = qctx_->ectx()->getResult(conjunct->outputVar());
+
+        DataSet expected;
+        expected.colNames = {"_path"};
+        {
+            Row row;
+            Path path;
+            path.src = Vertex("1", {});
+            path.steps.emplace_back(Step(Vertex("2", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("4", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("5", {}), 1, "edge1", 0, {}));
+            row.values.emplace_back(std::move(path));
+            expected.rows.emplace_back(std::move(row));
+        }
+        {
+            Row row;
+            Path path;
+            path.src = Vertex("1", {});
+            path.steps.emplace_back(Step(Vertex("2", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("4", {}), 1, "edge1", 1, {}));
+            path.steps.emplace_back(Step(Vertex("5", {}), 1, "edge1", 0, {}));
+            row.values.emplace_back(std::move(path));
+            expected.rows.emplace_back(std::move(row));
+        }
+
+        EXPECT_EQ(result.value().getDataSet(), expected);
+        EXPECT_EQ(result.state(), Result::State::kSuccess);
+    }
+}
+
+TEST_F(ConjunctPathTest, BiBFSFourStepsPath) {
+    auto* conjunct =
+        ConjunctPath::make(qctx_.get(), nullptr, nullptr, ConjunctPath::PathKind::kBiBFS);
+    conjunct->setLeftVar("forward1");
+    conjunct->setRightVar("backward4");
+    conjunct->setColNames({"_path"});
+
+    auto conjunctExe = std::make_unique<ConjunctPathExecutor>(conjunct, qctx_.get());
+
+    {
+        auto future = conjunctExe->execute();
+        auto status = std::move(future).get();
+        EXPECT_TRUE(status.ok());
+        auto& result = qctx_->ectx()->getResult(conjunct->outputVar());
+
+        DataSet expected;
+        expected.colNames = {"_path"};
+        EXPECT_EQ(result.value().getDataSet(), expected);
+        EXPECT_EQ(result.state(), Result::State::kSuccess);
+    }
+
+    {
+        {
+            // 2->6@0
+            // 2->6@1
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"6", Edge("2", "6", 1, "edge1", 0, {})};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            {
+                Row row;
+                row.values = {"6", Edge("2", "6", 1, "edge1", 1, {})};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("forward1", ResultBuilder().value(ds1).finish());
+        }
+        {
+            // 4->6@0
+            DataSet ds1;
+            ds1.colNames = {kVid, "edge"};
+            {
+                Row row;
+                row.values = {"6", Edge("4", "6", -1, "edge1", 0, {})};
+                ds1.rows.emplace_back(std::move(row));
+            }
+            qctx_->ectx()->setResult("backward4", ResultBuilder().value(ds1).finish());
+        }
+        auto future = conjunctExe->execute();
+        auto status = std::move(future).get();
+        EXPECT_TRUE(status.ok());
+        auto& result = qctx_->ectx()->getResult(conjunct->outputVar());
+
+        DataSet expected;
+        expected.colNames = {"_path"};
+        {
+            Row row;
+            Path path;
+            path.src = Vertex("1", {});
+            path.steps.emplace_back(Step(Vertex("2", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("6", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("4", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("5", {}), 1, "edge1", 0, {}));
+            row.values.emplace_back(std::move(path));
+            expected.rows.emplace_back(std::move(row));
+        }
+        {
+            Row row;
+            Path path;
+            path.src = Vertex("1", {});
+            path.steps.emplace_back(Step(Vertex("2", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("6", {}), 1, "edge1", 1, {}));
+            path.steps.emplace_back(Step(Vertex("4", {}), 1, "edge1", 0, {}));
+            path.steps.emplace_back(Step(Vertex("5", {}), 1, "edge1", 0, {}));
+            row.values.emplace_back(std::move(path));
+            expected.rows.emplace_back(std::move(row));
+        }
+
+        EXPECT_EQ(result.value().getDataSet(), expected);
+        EXPECT_EQ(result.state(), Result::State::kSuccess);
+    }
+}
+}  // namespace graph
+}  // namespace nebula
diff --git a/src/executor/test/TestMain.cpp b/src/executor/test/TestMain.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..23b013e43d493379a9dcc9534543368a020feeeb
--- /dev/null
+++ b/src/executor/test/TestMain.cpp
@@ -0,0 +1,18 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include <gtest/gtest.h>
+
+#include "common/base/Base.h"
+
+
+int main(int argc, char** argv) {
+    testing::InitGoogleTest(&argc, argv);
+    folly::init(&argc, &argv, true);
+    google::SetStderrLogging(google::INFO);
+
+    return RUN_ALL_TESTS();
+}
diff --git a/src/optimizer/OptGroup.cpp b/src/optimizer/OptGroup.cpp
index 4a055ee031225cd1742faa0b678a7a98996b8753..cb97fd1f9aded0cae1aaefadaeccebd8a34253b8 100644
--- a/src/optimizer/OptGroup.cpp
+++ b/src/optimizer/OptGroup.cpp
@@ -169,8 +169,8 @@ const PlanNode *OptGroupExpr::getPlan() const {
         case 2: {
             DCHECK_EQ(dependencies_.size(), 2U);
             auto bNode = static_cast<BiInputNode *>(node_);
-            bNode->setLeft(dependencies_[0]->getPlan());
-            bNode->setRight(dependencies_[1]->getPlan());
+            bNode->setLeftDep(dependencies_[0]->getPlan());
+            bNode->setRightDep(dependencies_[1]->getPlan());
             break;
         }
     }
diff --git a/src/planner/Algo.cpp b/src/planner/Algo.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..14453bdf49f5b790e92d21821932da0657abecf2
--- /dev/null
+++ b/src/planner/Algo.cpp
@@ -0,0 +1,13 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#include "planner/Algo.h"
+
+namespace nebula {
+namespace graph {
+
+}  // namnspace graph
+}  // namespace nebula
diff --git a/src/planner/Algo.h b/src/planner/Algo.h
new file mode 100644
index 0000000000000000000000000000000000000000..b33018717299b81a06d6924a06efd51e4cd3f557
--- /dev/null
+++ b/src/planner/Algo.h
@@ -0,0 +1,61 @@
+/* Copyright (c) 2020 vesoft inc. All rights reserved.
+ *
+ * This source code is licensed under Apache 2.0 License,
+ * attached with Common Clause Condition 1.0, found in the LICENSES directory.
+ */
+
+#ifndef PLANNER_ALGO_H_
+#define PLANNER_ALGO_H_
+
+#include "common/base/Base.h"
+#include "context/QueryContext.h"
+#include "planner/PlanNode.h"
+
+namespace nebula {
+namespace graph {
+class ProduceSemiShortestPath : public PlanNode {
+};
+
+class BFSShortestPath : public SingleInputNode {
+public:
+    static BFSShortestPath* make(QueryContext* qctx, PlanNode* input) {
+        return qctx->objPool()->add(new BFSShortestPath(qctx->genId(), input));
+    }
+
+private:
+    BFSShortestPath(int64_t id, PlanNode* input)
+        : SingleInputNode(id, Kind::kBFSShortest, input) {}
+};
+
+class ConjunctPath : public BiInputNode {
+public:
+    enum class PathKind : uint8_t {
+        kBiBFS,
+        kBiDijkstra,
+        kFloyd,
+        kAllPath,
+    };
+
+    static ConjunctPath* make(QueryContext* qctx,
+                              PlanNode* left,
+                              PlanNode* right,
+                              PathKind pathKind) {
+        return qctx->objPool()->add(new ConjunctPath(qctx->genId(), left, right, pathKind));
+    }
+
+    PathKind pathKind() const {
+        return pathKind_;
+    }
+
+private:
+    ConjunctPath(int64_t id, PlanNode* left, PlanNode* right, PathKind pathKind)
+        : BiInputNode(id, Kind::kConjunctPath, left, right) {
+        pathKind_ = pathKind;
+    }
+
+    PathKind pathKind_;
+};
+
+}  // namespace graph
+}  // namespace nebula
+#endif  // PLANNER_ALGO_H_
diff --git a/src/planner/ExecutionPlan.cpp b/src/planner/ExecutionPlan.cpp
index 3730bd97d45ef4a258692cebd4bbc15984bf4563..7229f5d4a03adae48035b27d74801e6d72719a20 100644
--- a/src/planner/ExecutionPlan.cpp
+++ b/src/planner/ExecutionPlan.cpp
@@ -36,7 +36,8 @@ static size_t makePlanNodeDesc(const PlanNode* node, cpp2::PlanDescription* plan
         }
         case PlanNode::Kind::kUnion:
         case PlanNode::Kind::kIntersect:
-        case PlanNode::Kind::kMinus: {
+        case PlanNode::Kind::kMinus:
+        case PlanNode::Kind::kConjunctPath: {
             auto bNode = static_cast<const BiInputNode*>(node);
             makePlanNodeDesc(bNode->left(), planDesc);
             makePlanNodeDesc(bNode->right(), planDesc);
diff --git a/src/planner/PlanNode.cpp b/src/planner/PlanNode.cpp
index 312634c95ded4b527e82ddfe179ca005485dfcec..913ff4a24237313c647dcbdfbe26c056da33ba35 100644
--- a/src/planner/PlanNode.cpp
+++ b/src/planner/PlanNode.cpp
@@ -176,6 +176,10 @@ const char* PlanNode::toString(PlanNode::Kind kind) {
             return "SetConfig";
         case Kind::kGetConfig:
             return "GetConfig";
+        case Kind::kBFSShortest:
+            return "BFSShortest";
+        case Kind::kConjunctPath:
+            return "ConjunctPath";
             // no default so the compiler will warning when lack
     }
     LOG(FATAL) << "Impossible kind plan node " << static_cast<int>(kind);
diff --git a/src/planner/PlanNode.h b/src/planner/PlanNode.h
index 321d7cc6f11f92587ad69a6c012fb320c56bd92b..2d31f595df6d298760a0bb7e739aab2a60f71879 100644
--- a/src/planner/PlanNode.h
+++ b/src/planner/PlanNode.h
@@ -104,6 +104,8 @@ public:
         kShowConfigs,
         kSetConfig,
         kGetConfig,
+        kBFSShortest,
+        kConjunctPath,
     };
 
     PlanNode(int64_t id, Kind kind);
@@ -237,11 +239,11 @@ protected:
 
 class BiInputNode : public PlanNode {
 public:
-    void setLeft(const PlanNode* left) {
+    void setLeftDep(const PlanNode* left) {
         setDep(0, left);
     }
 
-    void setRight(const PlanNode* right) {
+    void setRightDep(const PlanNode* right) {
         setDep(1, right);
     }
 
@@ -274,12 +276,21 @@ public:
 protected:
     BiInputNode(int64_t id, Kind kind, PlanNode* left, PlanNode* right)
         : PlanNode(id, kind) {
-        DCHECK(left != nullptr);
-        DCHECK(right != nullptr);
-        dependencies_.emplace_back(left);
-        dependencies_.emplace_back(right);
-        inputVars_.emplace_back(left->outputVar());
-        inputVars_.emplace_back(right->outputVar());
+        if (left != nullptr) {
+            dependencies_.emplace_back(left);
+            inputVars_.emplace_back(left->outputVar());
+        } else {
+            dependencies_.emplace_back();
+            inputVars_.emplace_back();
+        }
+
+        if (right != nullptr) {
+            dependencies_.emplace_back(right);
+            inputVars_.emplace_back(right->outputVar());
+        } else {
+            dependencies_.emplace_back();
+            inputVars_.emplace_back();
+        }
     }
 };
 
diff --git a/src/planner/Query.cpp b/src/planner/Query.cpp
index 7de517ebabe3930c23b2b5d6ec93977f021caf9e..f854840b5ac5c21e3fbc197c9c7a56a7e3a46106 100644
--- a/src/planner/Query.cpp
+++ b/src/planner/Query.cpp
@@ -128,15 +128,19 @@ std::unique_ptr<cpp2::PlanNodeDescription> DataCollect::explain() const {
     addDescription("inputVars", folly::toJson(util::toJson(inputVars_)), desc.get());
     switch (collectKind_) {
         case CollectKind::kSubgraph: {
-            addDescription("kind", "subgraph", desc.get());
+            addDescription("kind", "SUBGRAPH", desc.get());
             break;
         }
         case CollectKind::kRowBasedMove: {
-            addDescription("kind", "row", desc.get());
+            addDescription("kind", "ROW", desc.get());
             break;
         }
         case CollectKind::kMToN: {
-            addDescription("kind", "m to n", desc.get());
+            addDescription("kind", "M TO N", desc.get());
+            break;
+        }
+        case CollectKind::kBFSShortest: {
+            addDescription("kind", "BFS SHORTEST", desc.get());
             break;
         }
     }
diff --git a/src/planner/Query.h b/src/planner/Query.h
index 5cf4072cd263eee7fee8787571ac45d7916dd1ca..3885c48e769854b35618153cd6fc2e32d10bf1a9 100644
--- a/src/planner/Query.h
+++ b/src/planner/Query.h
@@ -838,6 +838,7 @@ public:
         kSubgraph,
         kRowBasedMove,
         kMToN,
+        kBFSShortest,
     };
 
     static DataCollect* make(QueryContext* qctx,
@@ -946,15 +947,6 @@ private:
     std::vector<Expression*>                hashKeys_;
     std::vector<Expression*>                probeKeys_;
 };
-
-class ProduceSemiShortestPath : public PlanNode {
-public:
-};
-
-class ConjunctPath : public PlanNode {
-public:
-};
-
 }  // namespace graph
 }  // namespace nebula
 #endif  // PLANNER_QUERY_H_
diff --git a/src/validator/FindPathValidator.cpp b/src/validator/FindPathValidator.cpp
index dd41123b9d93ace77a21f4316004e4adbeeae617..1d5d4fd8540b4474ca5c9f2937943f401ef4bfe7 100644
--- a/src/validator/FindPathValidator.cpp
+++ b/src/validator/FindPathValidator.cpp
@@ -5,6 +5,9 @@
  */
 
 #include "validator/FindPathValidator.h"
+
+#include "common/expression/VariableExpression.h"
+#include "planner/Algo.h"
 #include "planner/Logic.h"
 
 namespace nebula {
@@ -22,10 +25,122 @@ Status FindPathValidator::validateImpl() {
 
 Status FindPathValidator::toPlan() {
     // TODO: Implement the path plan.
-    auto* passThrough = PassThroughNode::make(qctx_, nullptr);
-    tail_ = passThrough;
-    root_ = tail_;
+    if (from_.vids.size() == 1 && to_.vids.size() == 1) {
+        return singlePairPlan();
+    } else {
+        auto* passThrough = PassThroughNode::make(qctx_, nullptr);
+        tail_ = passThrough;
+        root_ = tail_;
+    }
+    return Status::OK();
+}
+
+Status FindPathValidator::singlePairPlan() {
+    auto* bodyStart = StartNode::make(qctx_);
+    auto* passThrough = PassThroughNode::make(qctx_, bodyStart);
+
+    auto* forward = bfs(passThrough, from_, false);
+    VLOG(1) << "forward: " << forward->outputVar();
+
+    auto* backward = bfs(passThrough, to_, true);
+    VLOG(1) << "backward: " << backward->outputVar();
+
+    auto* conjunct =
+        ConjunctPath::make(qctx_, forward, backward, ConjunctPath::PathKind::kBiBFS);
+    conjunct->setLeftVar(forward->outputVar());
+    conjunct->setRightVar(backward->outputVar());
+    conjunct->setColNames({"_path"});
+
+    auto* loop = Loop::make(
+        qctx_, nullptr, conjunct, buildBfsLoopCondition(steps_.steps, conjunct->outputVar()));
+
+    auto* dataCollect = DataCollect::make(
+        qctx_, loop, DataCollect::CollectKind::kBFSShortest, {conjunct->outputVar()});
+    dataCollect->setColNames({"_path"});
+
+    root_ = dataCollect;
+    tail_ = loop;
     return Status::OK();
 }
+
+void FindPathValidator::buildStart(const Starts& starts,
+                                   std::string& startVidsVar,
+                                   PlanNode* dedupStartVid,
+                                   Expression* src) {
+    if (!starts.vids.empty() && starts.srcRef == nullptr) {
+        buildConstantInput(starts, startVidsVar, src);
+    } else {
+        dedupStartVid = buildRuntimeInput();
+        startVidsVar = dedupStartVid->outputVar();
+    }
+}
+
+PlanNode* FindPathValidator::bfs(PlanNode* dep, const Starts& starts, bool reverse) {
+    std::string startVidsVar;
+    Expression* vids = nullptr;
+    buildConstantInput(starts, startVidsVar, vids);
+
+    DCHECK(!!vids);
+    auto* gn = GetNeighbors::make(qctx_, dep, space_.id);
+    gn->setSrc(vids);
+    gn->setEdgeProps(buildEdgeKey(reverse));
+    gn->setInputVar(startVidsVar);
+
+    auto* bfs = BFSShortestPath::make(qctx_, gn);
+    bfs->setInputVar(gn->outputVar());
+    bfs->setColNames({"_vid", "edge"});
+    bfs->setOutputVar(startVidsVar);
+
+    DataSet ds;
+    ds.colNames = {"_vid", "edge"};
+    Row row;
+    row.values.emplace_back(starts.vids.front());
+    row.values.emplace_back(Value::kEmpty);
+    ds.rows.emplace_back(std::move(row));
+    qctx_->ectx()->setResult(startVidsVar, ResultBuilder().value(Value(std::move(ds))).finish());
+
+    return bfs;
+}
+
+Expression* FindPathValidator::buildBfsLoopCondition(uint32_t steps, const std::string& pathVar) {
+    // ++loopSteps{0} <= (steps/2+steps%2) && size(pathVar) == 0
+    auto loopSteps = vctx_->anonVarGen()->getVar();
+    qctx_->ectx()->setValue(loopSteps, 0);
+
+    auto* nSteps = new RelationalExpression(
+        Expression::Kind::kRelLE,
+        new UnaryExpression(
+            Expression::Kind::kUnaryIncr,
+            new VersionedVariableExpression(new std::string(loopSteps), new ConstantExpression(0))),
+        new ConstantExpression(static_cast<int32_t>(steps / 2 + steps % 2)));
+
+    auto* args = new ArgumentList();
+    args->addArgument(std::make_unique<VariableExpression>(new std::string(pathVar)));
+    auto* pathEmpty =
+        new RelationalExpression(Expression::Kind::kRelEQ,
+                                 new FunctionCallExpression(new std::string("size"), args),
+                                 new ConstantExpression(0));
+    auto* notFoundPath = new LogicalExpression(
+        Expression::Kind::kLogicalOr,
+        new RelationalExpression(Expression::Kind::kRelEQ,
+                                 new VariableExpression(new std::string(pathVar)),
+                                 new ConstantExpression(Value())),
+        pathEmpty);
+    return qctx_->objPool()->add(
+        new LogicalExpression(Expression::Kind::kLogicalAnd, nSteps, notFoundPath));
+}
+
+GetNeighbors::EdgeProps FindPathValidator::buildEdgeKey(bool reverse) {
+    auto edgeProps = std::make_unique<std::vector<storage::cpp2::EdgeProp>>(
+                over_.edgeTypes.size());
+    std::transform(over_.edgeTypes.begin(), over_.edgeTypes.end(), edgeProps->begin(),
+                [reverse](auto& type) {
+                    storage::cpp2::EdgeProp ep;
+                    ep.type = reverse ? -type : type;
+                    ep.props = {kDst, kType, kRank};
+                    return ep;
+                });
+    return edgeProps;
+}
 }  // namespace graph
 }  // namespace nebula
diff --git a/src/validator/FindPathValidator.h b/src/validator/FindPathValidator.h
index e9769284a78f37c74429e6fb36e3bb2b8b5bc59e..77f06975f7be933b857d97c38e0065a1ba431f2b 100644
--- a/src/validator/FindPathValidator.h
+++ b/src/validator/FindPathValidator.h
@@ -13,7 +13,6 @@
 namespace nebula {
 namespace graph {
 
-
 class FindPathValidator final : public TraversalValidator {
 public:
     FindPathValidator(Sentence* sentence, QueryContext* context)
@@ -24,9 +23,21 @@ private:
 
     Status toPlan() override;
 
+    Status singlePairPlan();
+
+    void buildStart(const Starts& starts,
+                    std::string& startVidsVar,
+                    PlanNode* dedupStartVid,
+                    Expression* src);
+
+    PlanNode* bfs(PlanNode* dep, const Starts& starts, bool reverse);
+
+    Expression* buildBfsLoopCondition(uint32_t steps, const std::string& pathVar);
+
+    GetNeighbors::EdgeProps buildEdgeKey(bool reverse);
+
 private:
     bool            isShortest_{false};
-    Starts          from_;
     Starts          to_;
     Over            over_;
     Steps           steps_;
diff --git a/src/validator/GetSubgraphValidator.cpp b/src/validator/GetSubgraphValidator.cpp
index 6c2c4103008992a69b0b615e44d53da7c42ec75b..501730177d868b3604cd9e442e9b6836b4e0678b 100644
--- a/src/validator/GetSubgraphValidator.cpp
+++ b/src/validator/GetSubgraphValidator.cpp
@@ -217,7 +217,7 @@ Status GetSubgraphValidator::toPlan() {
     std::string startVidsVar;
     SingleInputNode* collectRunTimeStartVids = nullptr;
     if (!from_.vids.empty() && from_.srcRef == nullptr) {
-        startVidsVar = buildConstantInput();
+        buildConstantInput(from_, startVidsVar, src_);
     } else {
         PlanNode* dedupStartVid = buildRuntimeInput();
         startVidsVar = dedupStartVid->outputVar();
diff --git a/src/validator/GoValidator.cpp b/src/validator/GoValidator.cpp
index 035c0c1b5124c77914ec2ee34f0d52d4ff656ceb..2311f0b2afbeda656bf323936d9d5a9648679af6 100644
--- a/src/validator/GoValidator.cpp
+++ b/src/validator/GoValidator.cpp
@@ -222,7 +222,7 @@ Status GoValidator::buildNStepsPlan() {
     std::string startVidsVar;
     PlanNode* dedupStartVid = nullptr;
     if (!from_.vids.empty() && from_.srcRef == nullptr) {
-        startVidsVar = buildConstantInput();
+        buildConstantInput(from_, startVidsVar, src_);
     } else {
         dedupStartVid = buildRuntimeInput();
         startVidsVar = dedupStartVid->outputVar();
@@ -277,7 +277,7 @@ Status GoValidator::buildMToNPlan() {
     std::string startVidsVar;
     PlanNode* dedupStartVid = nullptr;
     if (!from_.vids.empty() && from_.srcRef == nullptr) {
-        startVidsVar = buildConstantInput();
+        buildConstantInput(from_, startVidsVar, src_);
     } else {
         dedupStartVid = buildRuntimeInput();
         startVidsVar = dedupStartVid->outputVar();
@@ -590,7 +590,7 @@ Status GoValidator::buildOneStepPlan() {
     std::string startVidsVar;
     PlanNode* dedupStartVid = nullptr;
     if (!from_.vids.empty() && from_.srcRef == nullptr) {
-        startVidsVar = buildConstantInput();
+        buildConstantInput(from_, startVidsVar, src_);
     } else {
         dedupStartVid = buildRuntimeInput();
         startVidsVar = dedupStartVid->outputVar();
diff --git a/src/validator/TraversalValidator.cpp b/src/validator/TraversalValidator.cpp
index 83d5a106cf38a77d146b3184ddf7080159d0f26c..ffaeca6d2292182be8226be37fa9e1b904090403 100644
--- a/src/validator/TraversalValidator.cpp
+++ b/src/validator/TraversalValidator.cpp
@@ -167,22 +167,21 @@ PlanNode* TraversalValidator::projectDstVidsFromGN(PlanNode* gn, const std::stri
     return dedupDstVids;
 }
 
-std::string TraversalValidator::buildConstantInput() {
-    auto input = vctx_->anonVarGen()->getVar();
+void TraversalValidator::buildConstantInput(const Starts& starts,
+                                            std::string& startVidsVar,
+                                            Expression*& vids) {
+    startVidsVar = vctx_->anonVarGen()->getVar();
     DataSet ds;
     ds.colNames.emplace_back(kVid);
-    for (auto& vid : from_.vids) {
+    for (auto& vid : starts.vids) {
         Row row;
         row.values.emplace_back(vid);
         ds.rows.emplace_back(std::move(row));
     }
-    qctx_->ectx()->setResult(input, ResultBuilder().value(Value(std::move(ds))).finish());
+    qctx_->ectx()->setResult(startVidsVar, ResultBuilder().value(Value(std::move(ds))).finish());
 
-    auto* vids = new VariablePropertyExpression(new std::string(input),
-                                                new std::string(kVid));
+    vids = new VariablePropertyExpression(new std::string(startVidsVar), new std::string(kVid));
     qctx_->objPool()->add(vids);
-    src_ = vids;
-    return input;
 }
 
 PlanNode* TraversalValidator::buildRuntimeInput() {
diff --git a/src/validator/TraversalValidator.h b/src/validator/TraversalValidator.h
index a1ada764ca89ecd1f2016a3d2ee31200b01f7851..28c1592b819d15633b9e91531dc9798721efd1e3 100644
--- a/src/validator/TraversalValidator.h
+++ b/src/validator/TraversalValidator.h
@@ -56,7 +56,7 @@ protected:
 
     PlanNode* projectDstVidsFromGN(PlanNode* gn, const std::string& outputVar);
 
-    std::string buildConstantInput();
+    void buildConstantInput(const Starts& starts, std::string& startVidsVar, Expression*& vids);
 
     PlanNode* buildRuntimeInput();
 
diff --git a/tests/common/nebula_test_suite.py b/tests/common/nebula_test_suite.py
index 47149c1e21bb6c43990b4cc7d6a3070e6c5eb008..c05460523c2d25681376ab2af8c1e4362b5266d2 100644
--- a/tests/common/nebula_test_suite.py
+++ b/tests/common/nebula_test_suite.py
@@ -486,40 +486,44 @@ class NebulaTestSuite(object):
         assert empty, msg
 
     @classmethod
-    def check_path_result(self, rows, expect):
+    def check_path_result_without_prop(self, rows, expect):
         msg = 'len(rows)[%d] != len(expect)[%d]' % (len(rows), len(expect))
         assert len(rows) == len(expect), msg
         for exp in expect:
-            path = ttypes.Path()
-            path.entry_list = []
-            for ecol, j in zip(exp, range(len(exp))):
-                if j % 2 == 0 or j == len(exp):
-                    pathEntry = ttypes.PathEntry()
-                    vertex = ttypes.Vertex()
-                    vertex.id = ecol
-                    pathEntry.set_vertex(vertex)
-                    path.entry_list.append(pathEntry)
+            path = CommonTtypes.Path()
+            path.steps = []
+            for col, j in zip(exp, range(len(exp))):
+                if j == 0:
+                    src = CommonTtypes.Vertex()
+                    src.vid = col
+                    src.tags = []
+                    path.src = src
                 else:
-                    assert len(ecol) == 2, \
-                        "invalid values size in expect result"
-                    pathEntry = ttypes.PathEntry()
-                    edge = ttypes.Edge()
-                    edge.type = ecol[0]
-                    edge.ranking = ecol[1]
-                    pathEntry.set_edge(edge)
-                    path.entry_list.append(pathEntry)
+                    assert len(col) == 3, \
+                        "{} invalid values size in expect result".format(exp.__repr__())
+                    step = CommonTtypes.Step()
+                    step.name = col[0]
+                    step.ranking = col[1]
+                    step.type = 1
+                    dst = CommonTtypes.Vertex()
+                    dst.vid = col[2]
+                    dst.tags = []
+                    step.dst = dst
+                    step.props = {}
+                    path.steps.append(step)
             find = False
             for row in rows:
                 assert len(row.values) == 1, \
                     "invalid values size in rows: {}".format(row)
-                assert row.values[0].getType()() == ttypes.Value.PATH, \
+                assert row.values[0].getType() == CommonTtypes.Value.PVAL, \
                     "invalid column path type: {}".format(row.values[0].getType()())
-                if row.values[0].get_path() == path:
+                if row.values[0].get_pVal() == path:
                     find = True
                     break
-            msg = self.check_format_str.format(row.values[0].get_path(), path)
+            msg = self.check_format_str.format(row.values[0].get_pVal(), path)
             assert find, msg
             rows.remove(row)
+        assert len(rows) == 0
 
     @classmethod
     def check_error_msg(self, resp, expect):
diff --git a/tests/query/v1/test_find_path.py b/tests/query/v1/test_find_path.py
new file mode 100644
index 0000000000000000000000000000000000000000..737e242e7457c634db5f7c99bf6a96fa1a424817
--- /dev/null
+++ b/tests/query/v1/test_find_path.py
@@ -0,0 +1,78 @@
+# --coding:utf-8--
+#
+# Copyright (c) 2020 vesoft inc. All rights reserved.
+#
+# This source code is licensed under Apache 2.0 License,
+# attached with Common Clause Condition 1.0, found in the LICENSES directory.
+
+from tests.common.nebula_test_suite import NebulaTestSuite
+
+class TestFindPath(NebulaTestSuite):
+    @classmethod
+    def prepare(self):
+        self.use_nba()
+        self.load_vertex_edge()
+
+    def cleanup():
+        pass
+
+    def test_single_pair_constant_input(self):
+        stmt = 'FIND SHORTEST PATH FROM "Tim Duncan" TO "Tony Parker" OVER like'
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["_path"],
+            "rows" : [
+                [b"Tim Duncan", (b"like", 0, b"Tony Parker")]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_path_result_without_prop(resp.data.rows, expected_data["rows"])
+
+        stmt = 'FIND SHORTEST PATH FROM "Tim Duncan" TO "LaMarcus Aldridge" OVER like'
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["_path"],
+            "rows" : [
+                [b"Tim Duncan", (b"like", 0, b"Tony Parker"), (b"like", 0, b"LaMarcus Aldridge")]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_path_result_without_prop(resp.data.rows, expected_data["rows"])
+
+        stmt = 'FIND SHORTEST PATH FROM "Tiago Splitter" TO "LaMarcus Aldridge" OVER like'
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["_path"],
+            "rows" : [
+                [b"Tiago Splitter", (b"like", 0, b"Tim Duncan"), (b"like", 0, b"Tony Parker"), (b"like", 0, b"LaMarcus Aldridge")]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_path_result_without_prop(resp.data.rows, expected_data["rows"])
+
+        stmt = 'FIND SHORTEST PATH FROM "Tiago Splitter" TO "LaMarcus Aldridge" OVER like, teammate'
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["_path"],
+            "rows" : [
+                [b"Tiago Splitter", (b"like", 0, b"Tim Duncan"), (b"teammate", 0, b"LaMarcus Aldridge")]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_path_result_without_prop(resp.data.rows, expected_data["rows"])
+
+        stmt = 'FIND SHORTEST PATH FROM "Tiago Splitter" TO "LaMarcus Aldridge" OVER *'
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["_path"],
+            "rows" : [
+                [b"Tiago Splitter", (b"like", 0, b"Tim Duncan"), (b"teammate", 0, b"LaMarcus Aldridge")]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_path_result_without_prop(resp.data.rows, expected_data["rows"])