Repository: iterative/dvc

Issue:
exp init experience
```
$ dvc -V
2.9.2
```


```
$ git init
$ dvc init
$ dvc exp init python train.py
ERROR: unexpected error - [Errno 2] No such file or directory: 'params.yaml'
```

- [x] Why is it `unexpected` error? It should be like “ERROR: hyperparameters file ‘params.yaml’ is missing”.  🤔  Why `dvc exp init` does not create it if it is required?
- [x] Do we really need [Errno 2]? In what cases the code helps users? This makes the message longer and breaks human readability.

```
$ dvc exp run
ERROR: failed to reproduce 'dvc.yaml': [Errno 2] No such file or directory: '/Users/dmitry/src/test/myproj/data'
```
- [x] full path. It is supposed to be relative. Why: 1) to make it short - the full path provides zero value in most of use cases; 2) it makes it hard to submit reports (some users are not comfortable to share their directories struture - See `'/Users/dmitry/src/test/` - bc it might contain confidential info like user and project names.

- [x] why `dvc exp init` checked `params.yaml` but didn’t check (or create) `data/` & `src/`?
```
$ mkdir data
$ dvc exp run
ERROR: failed to reproduce 'dvc.yaml': [Errno 2] No such file or directory: '/Users/dmitry/src/test/myproj/src'
```

- [x] the same questions - [Errno 2], Full path.

- [x] why it was not checked in `dvc init`? Or was not created?

```
$ mkdir src
$ dvc exp run
…
python: can't open file 'train.py': [Errno 2] No such file or directory
ERROR: failed to reproduce 'dvc.yaml': failed to run: python train.py, exited with 2
```

- [x]  “exited with 2” Is it useful information? Where the exit code 2 belongs to? It might confuse users. Error code can be in debug errors not here.

- [x] Another issue - ideally, we should have a reference to stage (a user might have multiple stages with the same or similar commands). The message might look like:  **ERROR: failed to execute stage ‘train’ from ‘dvc.yaml’**


From a clean git repo:
```
$ dvc init
$ dvc exp init -i
…
Path to a parameters file [params.yaml, n to omit]:
'params.yaml' does not exist. Please retry with an existing parameters file.
```

- [x] If I do not have `params.yaml` I stuck here. Why don’t we offer to create an empty params file?


- [ ] **A kind of conclusion**: `dvc exp init` is not fully supported without interactive mode.
It looks like today the only supported flow is — interactive. The default `dvc init` has multiple assumptions about the project structure and it is not clear how a user should know about the assumptions. Why `dvc init` won’t create all the directories & files? This was a part of the initial idea - to run a single cmd for a given train.py in a live coding section while `dvc init` wraps it to an experiment.

This is how it can look like:
```
$ dvc init python train.py
Creating experiment project structure:
├── data/
├── metrics.json
├── models/
├── params.yaml
├── plots/
└── src/
Created `dvc.yaml` file with ‘train’ stage. To run, use "dvc exp run".
```


Epic: exp init
Parent issue for lightweight command for initializing experiments.

Linked issues:

- [x] #6441 
- [x] #6442 
- [x] ~~#6443~~
- [x] #6444 
- [x] #6445 

#### Small tasks that are left to do:
- [x] Autostaging dvc.yaml/.gitignore/params.yaml (https://github.com/iterative/dvc/issues/6446#issuecomment-943369189)
- [ ] `init` dvc repo if uninitialized (https://github.com/iterative/dvc/issues/6446#issuecomment-946937051)
- [x] Try to ask for commands before asking for paths (https://github.com/iterative/dvc/pull/6750#issuecomment-950854975)
- [x] Use `train` as a default name? (https://github.com/iterative/dvc/pull/6750#issuecomment-950854975)
- [ ] Raise issue if dvc is initialized with `--no-scm` or if the project is not `git init`-ed.
- [x] https://github.com/iterative/dvc/issues/6865
- [ ] Add `data` as a dvc-tracked directory (https://github.com/iterative/dvc/issues/6446#issuecomment-943369189)
- [x] Make confirmation prompt at the end look visually similar to other prompts.
- [x] Update docs after https://github.com/iterative/dvc.org/issues/2967 is merged.
- [x] Add tests/QA and improve CLI help message (if any).
- [x] Improve messaging/UI/guidance/prompts on the command.


Discussion:
@dmpetrov I broke this up into 3 related issues mentioned above to make it more manageable to take action on these. Please add anything missed from those, and we can continue discussion there. 🙏 
After speaking with @dmpetrov, one of the high-level issues is that the defaults for `dvc exp init` are asking the user for too much, too soon, and expecting/assuming too much from the user. Some ideas on how we can fix it:

1. Make `dvc exp init -i` the default.
2. Make `dvc exp init --explicit` the default.
3. Make `dvc exp init -i` the default if no command argument is provided, otherwise make `dvc exp init --explicit` the default.

@skshetry Let me know if you have thoughts.

In the interactive mode, this might also relate to earlier discussions about whether a blank entry should be skipped or use the default path.
Other options:

4. Create another `type` like `simple` or `blank` that creates a stage with only a command (no deps or outputs). This might also allow us to get rid of `--explicit`.
5. Allow for `--data`, `--params`, etc. to be passed without arguments, in which case they will be added to the stage with default values. This would be useful if `--explicit` is the default or if there is blank stage `type`. If we do this, we probably need short flags for these to enable something like `dvc exp init -dp`.

IMO making `--explicit` the default might make the command too close to `dvc stage add`, but the other ideas could all work.
> IMO making `--explicit` the default might make the command too close to `dvc stage add`, but the other ideas could all work.

Agree. There are some good options but this one seems like the best one.
> Agree. There are some good options but this one seems like the best one.

@dmpetrov We might be saying slightly different things 😄. However, if you want to start from this simple default, I think the best way to achieve this is option 4 above. We can have 3 template types: `default` (a stage with only a command), `ml` (the current default), and `dl`.

Why add another template type? One goal of `dvc exp init` was to introduce an easy way to setup typical project structures, such as deep learning with checkpoints and dvclive since it's confusing to configure manually. If we always start with a blank stage, there's no real difference between these template types (at least in non-interactive mode), and their purpose becomes confusing.
@dberenbaum oh, right 🙂 `--explicit` is going to be the same as `stage add`. It is not a problem to be the same, the problem is it does not serve the purpose of `exp init` - being a shortcut/cookiecutter for experiments.

There are couple more questions:
- [ ] `--type=dl` has the same issue as the default one - `dvc exp init --type dl` is not functional until you create all the data/src/metrics files and dirs.

- [ ] Why there are no plots in `--type=dl` (we have plots in the regular one)?

- [ ] `--interactive` also does not create a proper structure for the default options. It is very easy to check the file and dirs for existence in interactive mode and ask if they'd like to create the files. That's what the user will see as the result:
```
$ dvc exp run
WARNING: Your workspace contains staged Git changes which will be unstaged before running this experiment.
ERROR: failed to reproduce 'dvc.yaml': [Errno 2] No such file or directory: '/Users/dmitry/src/test/exp_init_i/data'
```

My opinion (similar to the Conclusion in the initial issue description) - `dvc exp init` (including `--interactive`) cannot successfully finish if the specified (or default) files and directory structure is not created. Otherwise, we just moving `exp init` issues to the laster `dvc exp run` stage where the issues cannot be solved automatically. So, we are basically moving the issues to the user's shoulders.
@dmpetrov It seems there are multiple use cases:
1. Taking a code script without dependencies or outputs and running it as an experiment. The blank template/`--explicit` default can solve it, or we can make some alias for `dvc stage add` to do this.
2. Adapting an existing project with parameters, metrics, data, etc. into a DVC experiment. Could you take a look at #7138 and provide your feedback? Creating empty files may still move the issues to the user's shoulders. Better messaging can give hints to users on what to do.

Another possible use case:
3. Adding dependencies and outputs gradually.

One possible way to do this is to allow `dvc exp init` to append new information to an existing stage. For example:

```
$ dvc exp init python train.py
Created train stage in dvc.yaml.
$ cat dvc.yaml
stages:
  train:
    cmd: python train.py
$ dvc exp init --data --models model.h5
Modified train stage in dvc.yaml.
$ cat dvc.yaml
stages:
  train:
    cmd: python train.py
    deps:
    - data
    outs:
    - model.h5
$ dvc exp init --data raw
ERROR: The specification conflicts with stage 'train' in 'dvc.yaml'. Use '--force' to overwrite.
$ dvc exp init -f --data raw
Modified train stage in dvc.yaml.
$ cat dvc.yaml
stages:
  train:
    cmd: python train.py
    deps:
    - raw
    outs:
    - model.h5 
```

Note that this isn't possible in `stage add` since every dep/out type may be passed multiple times in a stage, making it impossible to know when to append vs. overwrite.
I was expecting to have a cookie-cutter based on a user's script. We need to generate a template with a predefined structure that users can use to adopt the project step by step. The predefined structure also serves an education goal - user knows possibilities and how to adopt these.

I think we have to have a proper cookie-cutter as one of the options of `dvc exp init`. It would be great to have this as the default one.

`--explicit` as default might be an easy starting point but we are losing the idea of cookie-cutter and educational part of it. As a result, we push people to read all the `exp` docs.

Adding dependencies and outputs gradually is a good idea but looks like overkill 🙂 As we discussed before, we should promote the idea of modifying dvc.yaml manually, not by commands.

In any of these scenarios, I don't think we should keep a user with not working `dvc exp run` (as much as we can). Missing files/dir should lead to command failure (or error messages with a repeat in `--interactive` mode).
> In any of these scenarios, I don't think we should keep a user with not working `dvc exp run` (as much as we can). Missing files/dir should lead to command failure (or error messages with a repeat in `--interactive` mode).

Right, I think everyone can agree with this. We can prioritize that for now while we continue to think through other ways to improve the experience.
We should still try to guide users on `exp run`, as these may still happen if `dvc.yaml` is modified by users. That does not mean that we cannot improve `exp init` message. 

Another problem that I see these days is that `exp init` has a concept of `data`/`models`/`source`, etc. which we don't have in `dvc.yaml` (which is translated into dependencies/outputs instead) which makes the successive `exp run` confusing.
If we make `--explicit` the default, we'll force users to iterate their stage through `dvc.yaml` later, I am not sure if this a good or a bad thing though, but they have different concepts that don't translate well (as mentioned above).
@skshetry good point about `--explicit`. A whole set of defaults might lead to a better default experience.
@dberenbaum and I discussed creating workspace structure on defaults, and showing workspace tree on non-interactive mode as we do for interactive modes. We also need to work on #4112 to support tracking everything inside of params file.
@dmpetrov mentioned in our discussion that `plots` is an advanced feature, so we may want to make it optional here in the `exp init`.
Should we convert this to a discussion after #7331 rather than close it completely? The immediate action points have been addressed, but I don't think everything has been resolved.

---

Looking at #7331, It might be helpful to separate inputs and outputs in the UI to clarify which will be created. No action needed now, but to me it seems confusing why some paths are created and others aren't.

@tapadipti shared her experience with `dvc exp init` and mentioned that it's confusing that for `data`, you have to do `dvc add`, but not for `models`.
Sure, let's turn it into discussion after #7331, I'll remove the closing marker. 

> @tapadipti shared her experience with dvc exp init and mentioned that it's confusing that for data, you have to do dvc add, but not for models.

Since we create `data` directory now (after #7331), it makes sense to also track them by defaults. It's a bit unclear if we should always do this or only when we are creating those directories ourselves during `exp init`.
I just remembered that I have an open PR for tracking dependencies in #6914, but haven't moved forward with it because it requires us to basically re-implement simpler `dvc add`. Tracking dependencies but without ensuring any graph correctness is going to be simple (duplicated outputs check, overlapped output, etc), but otherwise we'll end up re-implementing `dvc add`.

We cannot reuse `dvc add` as it's more complex, has its own messy output and progress bar, and in `dvc exp init`, we need to check correctness together with the pipeline stage.
> (duplicated outputs check, overlapped output, etc)

Sorry, can you explain? I didn't get your point.
@dberenbaum, we need to check if the data output is in conflict with other existing .dvc files or stages and with the pipeline stage that we are going to generate. The checks that we do are:

1. That the graph is acyclic (is a DAG),
2. That the output is not duplicated,
3. That the output does not overlap with an already existing output, and 
4. That the .dvc file to be generated is not inside the output of a different stage, and
5. The .dvc file does not exist already.

This is a bit complicated and involves collecting all `dvc.yaml` and `.dvc` files. #6914 does already handle all of that, but it does so by duplicating what `dvc add` does already. I'm just looking for solving this in a better way, or waiting to come up with a better solution.

We could however simplify the implementation by choosing not to verify, as it is also done later on every operation.
Let's leave it for now since I believe some of the planned data management changes could impact this, and it might make more sense to revisit later.
Maybe we can have a label for these summaries?
From the @dberenbaum's comment on https://github.com/iterative/dvc/pull/6681#issuecomment-928180859.

>How do you anticipate this (dvclive scenario) working for non-interactive mode?
>
> I'm still wondering whether the template option is needed (solely as a cli option, not as an actual template; it could be renamed) to separate the dvclive scenario and provide flexibility to add other scenarios in the future. What do you think?


One way may be to make `--live` mandatory for dvclive scenario unless `live` is set on the config (though there's a question about what to do if `metrics` and `plots` are also specified in the config). Maybe keeping `live` from config is also a way to go.

Another would be to create a `--template` or something similar flag (`--with={live, etc.}`)? 


> Another would be to create a `--template` or something similar flag (`--with={live, etc.}`)?

Although it adds another option, I like this mainly because it's more explicit. Now that we don't have templates, it's less transparent what the command and various options are doing. Having a separate option for this suggests that it's not just another path to add but actually changes the stage configuration. We can also further explain it in the help text for that option.

On a similar note, at least interactive mode should probably have a confirmation so that users can see what will be added to `dvc.yaml` beforehand. There could be an option to do the same outside of interactive mode (`--dry`?). Not sure if this has been discussed before for `dvc stage add`.
Tasks that remains:
- [ ] Improve UI/CLI, messaging, and prompts
- [x] Add config support
- [ ] Add tests/QA
- [ ] ~~https://github.com/iterative/dvc/issues/6168~~
- [ ] ~~https://github.com/iterative/dvc/issues/6605~~
Might be worth revisiting https://github.com/iterative/dvcyaml-schema/issues/7 to better support the workflow: 

`dvc exp init` for simple cases / basic bootstrapping -> manually edit `dvc.yaml` for customization?
With dvcyaml-schema, my goal was to have same schema for JSONSchema and validation. We have been using pydantic for JSONSchema generation and voluptuous for validation.

There are few issues with using pydantic:
1. It does not have a good support for validating dictionaries with varying keys. `stages` in dvc.yaml is a dictionary with key being stage name and value being stage information. Pydantic is not very good at representing this. Similarly, `deps`/`outs` are also list of dictionaries.
2. It is almost twice as slow as voluptuous, because it uses type-hints.
3. It coerces types (though you can make it stricter). It may be a good thing sometimes, but it does make it quite tricky to make voluptuous-based schema to be compatible with pydantic-based schema.

In short term, I think it's better to move pydantic schema to dvc, and support both of those schema. 

Voluptuous also has negatives, that it's not very actively maintained, and is a bit hard to extend to have custom validations
compared to pydantic (whose DX is great). 

Also, looking for `cattrs` is another option, but I believe it'll have same issues as `pydantic`. Though migrating to any other libraries may take longer time, as it's quite hard to guarantee compatibilities (we rarely test unhappy paths for dvc.yaml) and the need for QA'ing.

We should probably include docs work also.
I noticed a couple of workflow issues when doing some QA of `dvc exp init`:

1. It's unclear when or what to `git add`. If I don't `git add` the `dvc.yaml` or the dependencies, `dvc exp init` works in the workspace but not in temp.
2. If I am adding a data dependency, I most likely want to `dvc add` it rather than `git add`. 

cc @pmrowla 
New/untracked files aren't pulled into the `--temp/--queue` temporary workspace by default.

from https://dvc.org/doc/user-guide/experiment-management/running-experiments#the-experiments-queue:

> Note that Git-ignored files/dirs are explicitly excluded from queued/temp runs to avoid committing unwanted files into Git (e.g. once successful experiments are persisted).
>
> 💡 To include untracked files, stage them with git add first (before dvc exp run) and git reset them afterwards.

It sounds like we need:
-  deps from `exp init` should get `.dvc` files (`dvc add`ed)
- `exp run --temp` should just automatically stage any untracked `.dvc` or `dvc.yaml` files (it seems like this is probably the desired default behavior in all cases, not just `exp init` specific ones?)
    - this doesn't address everything and there may be other untracked files that users actually need to manually stage for their experiment, but we can't really automate behavior for non-dvc-related files
> deps from `exp init` should get `.dvc` files (`dvc add`ed)

This is only true for the data dependency, since the code and params dependencies probably should be git tracked. We might just start with a hint here. Doing a `dvc add` automatically might be unexpected and make `dvc exp init` much slower. I don't love the hint idea, but it's an easy starting point. 🤔 

> `exp run --temp` should just automatically stage any untracked `.dvc` or `dvc.yaml` files (it seems like this is probably the desired default behavior in all cases, not just `exp init` specific ones?)
> 
> * this doesn't address everything and there may be other untracked files that users actually need to manually stage for their experiment, but we can't really automate behavior for non-dvc-related files

👍 Can we also try to add (not force but at least do a regular git add) any other dependencies of the experiment?
Discussed with @pmrowla and he noted that iterating over stages and git-adding everything during `exp run` could be time-consuming when doing things like queuing an experiment. It seems easier to git add during `exp init` since we already have all the stage info. Thoughts on adding a git add operation at the end of the command @skshetry? This might make sense as a `dvc stage add` option along the lines of #5932.

---

Would it be possible to mark the `data` dependency as dvc-tracked in a lightweight way? For example, add `data` to `.gitignore` and create a `data.dvc` file with contents:

```
outs:
- path: data
```

This also seems like it might be useful generally as an option in `dvc add` (similar to https://git-scm.com/docs/git-add#Documentation/git-add.txt---intent-to-add but likely more useful given the size of data dvc handles). Thoughts/suggestions?
Till now, we have considered dependency to be a git-tracked or dvc-tracked dependency, so we do not track them by default. But makes sense to do for `data` in `exp init`.

Would it be confusing to create `data.dvc` and `dvc.yaml` files?

Regarding the `git add` by default, we have `core.autostage` config that does this automatically. I don't see any issues with doing this by default (it's only `dvc.yaml`/`.gitignore` right?).
@dberenbaum, should we also `dvc init` by default on `dvc exp init`?
> Would it be confusing to create data.dvc and dvc.yaml files?

Ideally, we could specify in `dvc.yaml` that `data` is a DVC-tracked dependency, but I think it's probably easier to add `data.dvc` than to figure out new `dvc.yaml` syntax for now, unless you have an idea to do that easily. Adding `data.dvc` might be awkward, but it also introduces a basic concept to users. What do you think?

> I don't see any issues with doing this by default (it's only dvc.yaml/.gitignore right?).

👍 

> @dberenbaum, should we also `dvc init` by default on `dvc exp init`?

👍 
@pmrowla, do we need to also stage `params` file? Or, does `experiments` does some magic for those?

EDIT: looks like we do need to stage that file ourselves.
Just wanted to upvote:

> Use train as a default name? (exp init: iterate on ui, tree and show to-be-generated yaml content #6750 (comment))

At least for the `--type live` option, having the name of the added stage (`live`) to be the same as one of the output fields feels a little strange.

And regardless of whether is a deep learning (`live`) experiment or not, the action to be performed in the stage would be the same (so `train` sounds good)
What about naming the types to match the use cases, like classical ml and deep learning? `ml` and `dl`? This might be more user-friendly since it focuses less on the products/features involved, plus `dvclive` might expand to non-deep learning use cases, which makes things even more confusing.
I'm sorry, I do not quite understand why `live` is a `dl` only thing?
Two reasons:

1. The stage includes checkpoints.
2. Dvclive was initially created for "live" monitoring of steps during training.

I would consider these to mostly be useful when doing deep learning, although maybe that's my personal bias. @karajan1001 do you see those being useful in other scenarios?

However, dvclive may become more of a general-purpose logger, in which case it may be useful in any ml scenario. That's one reason to consider renaming `dvc exp init --type live`.
My 2c: 

`--type` may be renamed to `--feature` for a more flexible approach. 

```
dvc exp init --feature live --feature some-future-feature
```

may place nicely together then. 
@iesahin, `dvc exp init` is targeted more in terms of _scenarios_ rather than features, which we have plenty of in `dvc stage add`.
@skshetry Can we go with `dl` or `deep` for now since that's the focus of the scenario? We don't even need to rename the default.
> `init` dvc repo if uninitialized
> Add data as a dvc-tracked directory

After discussion, these are being de-prioritized for now since they are not simple changes and are not strictly necessary.


> Can we go with dl or deep for now since that's the focus of the scenario? We don't even need to rename the default.

`deep` does not convey enough information, I think. Is `DL` a common abbreviation?


> Is `DL` a common abbreviation?

Yeah, it's not like ML, but it's fairly common. Google "deep learning abbreviation."
@dberenbaum, with `--dl`, I don't think `dl` as a default stage name makes sense. Maybe we should go with `train` then? 
What's the status @skshetry?
> Raise issue if dvc is initialized with --no-scm or if the project is not git init-ed.
> should we also dvc init by default on dvc exp init?

I think `exp init` could even do both `git/dvc init` if needed (https://github.com/iterative/dvc/issues/7109). But definitely at least the latter 👍 
Also, I noticed `params.yaml` is the only required part of the default project structure (even if it's empty). Should `exp init` create an empty one (with some comment on top perhaps) if it's not there? Otherwise it fails.
> Also, I noticed `params.yaml` is the only required part of the default project structure (even if it's empty). Should `exp init` create an empty one (with some comment on top perhaps) if it's not there? Otherwise it fails.

@jorgeorpinel See my response in https://github.com/iterative/dvc.org/pull/3071#discussion_r766966189.
> The params section of dvc.yaml currently expects keys from a file

What about generating a dvc.yaml without `params` if there's no params.yaml file?
related: https://github.com/iterative/dvc/issues/7130

Resolving patch:
diff --git a/dvc/commands/experiments/init.py b/dvc/commands/experiments/init.py
--- a/dvc/commands/experiments/init.py
+++ b/dvc/commands/experiments/init.py
@@ -1,5 +1,6 @@
 import argparse
 import logging
+from typing import TYPE_CHECKING, List
 
 from funcy import compact
 
@@ -8,6 +9,11 @@
 from dvc.exceptions import InvalidArgumentError
 from dvc.ui import ui
 
+if TYPE_CHECKING:
+    from dvc.dependency import Dependency
+    from dvc.stage import PipelineStage
+
+
 logger = logging.getLogger(__name__)
 
 
@@ -29,7 +35,6 @@ class CmdExperimentsInit(CmdBase):
         "plots": PLOTS,
         "live": DVCLIVE,
     }
-    EXP_LINK = "https://s.dvc.org/g/exp/run"
 
     def run(self):
         from dvc.commands.stage import parse_cmd
@@ -58,7 +63,7 @@ def run(self):
             }
         )
 
-        initialized_stage = init(
+        initialized_stage, initialized_deps = init(
             self.repo,
             name=self.args.name,
             type=self.args.type,
@@ -67,32 +72,42 @@ def run(self):
             interactive=self.args.interactive,
             force=self.args.force,
         )
+        self._post_init_display(initialized_stage, initialized_deps)
+        if self.args.run:
+            self.repo.experiments.run(targets=[initialized_stage.addressing])
+        return 0
 
-        text = ui.rich_text.assemble(
-            "Created ",
-            (self.args.name, "bright_blue"),
-            " stage in ",
-            ("dvc.yaml", "green"),
-            ".",
+    def _post_init_display(
+        self, stage: "PipelineStage", new_deps: List["Dependency"]
+    ) -> None:
+        from dvc.utils import humanize
+
+        path_fmt = "[green]{0}[/green]".format
+        if new_deps:
+            deps_paths = humanize.join(map(path_fmt, new_deps))
+            ui.write(f"Creating dependencies: {deps_paths}", styled=True)
+
+        ui.write(
+            f"Creating [b]{self.args.name}[/b] stage in [green]dvc.yaml[/]",
+            styled=True,
         )
-        if not self.args.run:
-            text.append_text(
-                ui.rich_text.assemble(
-                    " To run, use ",
-                    ('"dvc exp run"', "green"),
-                    ".\nSee ",
-                    (self.EXP_LINK, "repr.url"),
-                    ".",
-                )
-            )
+        if stage.outs or not self.args.run:
+            # separate the above status-like messages with help/tips section
+            ui.write(styled=True)
 
-        ui.write(text, styled=True)
-        if self.args.run:
-            return self.repo.experiments.run(
-                targets=[initialized_stage.addressing]
-            )
+        if stage.outs:
+            outs_paths = humanize.join(map(path_fmt, stage.outs))
+            tips = f"Ensure your experiment command creates {outs_paths}."
+            ui.write(tips, styled=True)
 
-        return 0
+        if not self.args.run:
+            ui.write(
+                'You can now run your experiment using [b]"dvc exp run"[/].',
+                styled=True,
+            )
+        else:
+            # separate between `exp.run` output and `dvc exp init` output
+            ui.write(styled=True)
 
 
 def add_parser(experiments_subparsers, parent_parser):
diff --git a/dvc/repo/experiments/init.py b/dvc/repo/experiments/init.py
--- a/dvc/repo/experiments/init.py
+++ b/dvc/repo/experiments/init.py
@@ -8,6 +8,7 @@
     Callable,
     Dict,
     Iterable,
+    List,
     Optional,
     TextIO,
     Tuple,
@@ -19,12 +20,11 @@
 from dvc.exceptions import DvcException
 from dvc.stage import PipelineStage
 from dvc.types import OptStr
-from dvc.utils import humanize
 
 if TYPE_CHECKING:
     from dvc.repo import Repo
     from dvc.dvcfile import DVCFile
-    from rich.tree import Tree
+    from dvc.dependency import Dependency
 
 from dvc.ui import ui
 
@@ -75,70 +75,41 @@ def _disable_logging(highest_level=logging.CRITICAL):
         logging.disable(previous_level)
 
 
-def build_workspace_tree(workspace: Dict[str, str], label: str) -> "Tree":
-    from rich.tree import Tree
-
-    tree = Tree(label, highlight=True)
-    for value in sorted(workspace.values()):
-        tree.add(f"[green]{value}[/green]")
-    return tree
-
-
-def display_workspace_tree(workspace: Dict[str, str], label: str) -> None:
-    d = workspace.copy()
-    d.pop("cmd", None)
-
-    if d:
-        ui.write(build_workspace_tree(d, label), styled=True)
-    ui.write(styled=True)
-
-
-PIPELINE_FILE_LINK = "https://s.dvc.org/g/pipeline-files"
-
-
 def init_interactive(
-    name: str,
     defaults: Dict[str, str],
     provided: Dict[str, str],
     validator: Callable[[str, str], Union[str, Tuple[str, str]]] = None,
     live: bool = False,
     stream: Optional[TextIO] = None,
 ) -> Dict[str, str]:
-    command = provided.pop("cmd", None)
-    prompts = lremove(provided.keys(), ["code", "data", "models", "params"])
-    prompts.extend(
-        lremove(provided.keys(), ["live"] if live else ["metrics", "plots"])
+    command_prompts = lremove(provided.keys(), ["cmd"])
+    dependencies_prompts = lremove(provided.keys(), ["code", "data", "params"])
+    outputs_prompts = lremove(
+        provided.keys(),
+        ["models"] + (["live"] if live else ["metrics", "plots"]),
     )
 
     ret: Dict[str, str] = {}
-    if prompts or command:
-        ui.error_write(
-            f"This command will guide you to set up a [bright_blue]{name}[/]",
-            "stage in [green]dvc.yaml[/].",
-            f"\nSee [repr.url]{PIPELINE_FILE_LINK}[/].\n",
-            styled=True,
-        )
-
-    if command:
-        ret["cmd"] = command
-    else:
-        ret.update(
-            compact(_prompts(["cmd"], allow_omission=False, stream=stream))
+    if "cmd" in provided:
+        ret["cmd"] = provided["cmd"]
+
+    for heading, prompts, allow_omission in (
+        ("", command_prompts, False),
+        ("Enter experiment dependencies.", dependencies_prompts, True),
+        ("Enter experiment outputs.", outputs_prompts, True),
+    ):
+        if prompts and heading:
+            ui.error_write(heading, styled=True)
+        response = _prompts(
+            prompts,
+            defaults=defaults,
+            allow_omission=allow_omission,
+            validator=validator,
+            stream=stream,
         )
+        ret.update(compact(response))
         if prompts:
             ui.error_write(styled=True)
-
-    if prompts:
-        ui.error_write(
-            "Enter the paths for dependencies and outputs of the command.",
-            styled=True,
-        )
-        ret.update(
-            compact(
-                _prompts(prompts, defaults, validator=validator, stream=stream)
-            )
-        )
-        ui.error_write(styled=True)
     return ret
 
 
@@ -189,7 +160,7 @@ def is_file(path: str) -> bool:
     return bool(ext)
 
 
-def init_deps(stage: PipelineStage) -> None:
+def init_deps(stage: PipelineStage) -> List["Dependency"]:
     from funcy import rpartial
 
     from dvc.dependency import ParamsDependency
@@ -198,10 +169,6 @@ def init_deps(stage: PipelineStage) -> None:
     new_deps = [dep for dep in stage.deps if not dep.exists]
     params, deps = lsplit(rpartial(isinstance, ParamsDependency), new_deps)
 
-    if new_deps:
-        paths = map("[green]{0}[/]".format, new_deps)
-        ui.write(f"Created {humanize.join(paths)}.", styled=True)
-
     # always create a file for params, detect file/folder based on extension
     # for other dependencies
     dirs = [dep.fs_path for dep in deps if not is_file(dep.fs_path)]
@@ -213,6 +180,8 @@ def init_deps(stage: PipelineStage) -> None:
         with localfs.open(path, "w", encoding="utf-8"):
             pass
 
+    return new_deps
+
 
 def init(
     repo: "Repo",
@@ -223,7 +192,7 @@ def init(
     interactive: bool = False,
     force: bool = False,
     stream: Optional[TextIO] = None,
-) -> PipelineStage:
+) -> Tuple[PipelineStage, List["Dependency"]]:
     from dvc.dvcfile import make_dvcfile
 
     dvcfile = make_dvcfile(repo, "dvc.yaml")
@@ -236,7 +205,6 @@ def init(
 
     if interactive:
         defaults = init_interactive(
-            name,
             validator=partial(validate_prompts, repo),
             defaults=defaults,
             live=with_live,
@@ -288,10 +256,9 @@ def init(
     with _disable_logging(), repo.scm_context(autostage=True, quiet=True):
         stage.dump(update_lock=False)
         stage.ignore_outs()
-        display_workspace_tree(context, "Using experiment project structure:")
-        init_deps(stage)
+        initialized_deps = init_deps(stage)
         if params:
             repo.scm_context.track_file(params)
 
     assert isinstance(stage, PipelineStage)
-    return stage
+    return stage, initialized_deps


Test patch:
diff --git a/tests/func/experiments/test_init.py b/tests/func/experiments/test_init.py
--- a/tests/func/experiments/test_init.py
+++ b/tests/func/experiments/test_init.py
@@ -30,7 +30,11 @@ def test_init_simple(tmp_dir, scm, dvc, capsys):
     assert main(["exp", "init", script]) == 0
     out, err = capsys.readouterr()
     assert not err
-    assert "Created train stage in dvc.yaml" in out
+    assert (
+        "Creating train stage in dvc.yaml\n\n"
+        "Ensure your experiment command creates metrics.json, plots and models"
+        '.\nYou can now run your experiment using "dvc exp run".'
+    ) in out
     assert (tmp_dir / "dvc.yaml").parse() == {
         "stages": {
             "train": {
@@ -77,7 +81,7 @@ def test_when_stage_force_if_already_exists(tmp_dir, dvc):
 
 
 @pytest.mark.parametrize("interactive", [True, False])
-def test_creates_params_file_by_default(tmp_dir, dvc, interactive, capsys):
+def test_creates_params_file_by_default(tmp_dir, dvc, interactive):
     init(
         dvc,
         interactive=interactive,
@@ -88,10 +92,6 @@ def test_creates_params_file_by_default(tmp_dir, dvc, interactive, capsys):
 
     assert (tmp_dir / "params.yaml").is_file()
     assert (tmp_dir / "params.yaml").parse() == {}
-    out, err = capsys.readouterr()
-    assert "Created src, data and params.yaml." in out
-    if interactive:
-        assert "'params.yaml' does not exist, the file will be created." in err
 
 
 def test_with_a_custom_name(tmp_dir, dvc):
@@ -152,7 +152,7 @@ def test_init_interactive_when_no_path_prompts_need_to_be_asked(
 
 def test_when_params_is_omitted_in_interactive_mode(tmp_dir, scm, dvc):
     (tmp_dir / "params.yaml").dump({"foo": 1})
-    inp = io.StringIO("python script.py\nscript.py\ndata\nmodels\nn")
+    inp = io.StringIO("python script.py\nscript.py\ndata\nn")
 
     init(
         dvc, interactive=True, stream=inp, defaults=CmdExperimentsInit.DEFAULTS
@@ -182,7 +182,7 @@ def test_when_params_is_omitted_in_interactive_mode(tmp_dir, scm, dvc):
 def test_init_interactive_params_validation(tmp_dir, dvc, capsys):
     tmp_dir.gen({"data": {"foo": "foo"}})
     (tmp_dir / "params.yaml").dump({"foo": 1})
-    inp = io.StringIO("python script.py\nscript.py\ndata\nmodels\ndata\n")
+    inp = io.StringIO("python script.py\nscript.py\ndata\ndata")
 
     init(
         dvc, stream=inp, interactive=True, defaults=CmdExperimentsInit.DEFAULTS
@@ -204,17 +204,17 @@ def test_init_interactive_params_validation(tmp_dir, dvc, capsys):
     assert (tmp_dir / "data").is_dir()
 
     out, err = capsys.readouterr()
+    assert not out
     assert (
         "Path to a parameters file [params.yaml, n to omit]: "
         "'data' is a directory. "
         "Please retry with an existing parameters file.\n"
         "Path to a parameters file [params.yaml, n to omit]:"
     ) in err
-    assert "Created script.py." in out
 
 
 def test_init_with_no_defaults_interactive(tmp_dir, dvc):
-    inp = io.StringIO("script.py\n" "data\n" "model\n" "n\n" "metric\n" "n\n")
+    inp = io.StringIO("script.py\n" "data\n" "n\n" "model\n" "metric\n" "n\n")
     init(
         dvc,
         defaults={},
@@ -247,8 +247,8 @@ def test_init_with_no_defaults_interactive(tmp_dir, dvc):
                 "python script.py\n"
                 "script.py\n"
                 "data\n"
-                "models\n"
                 "params.yaml\n"
+                "models\n"
                 "metrics.json\n"
                 "plots\n"
                 "y"
@@ -290,11 +290,10 @@ def test_init_default(tmp_dir, scm, dvc, interactive, overrides, inp, capsys):
     assert scm.is_ignored("models")
     out, err = capsys.readouterr()
 
+    assert not out
     if interactive:
         assert "'script.py' does not exist, the file will be created." in err
         assert "'data' does not exist, the directory will be created." in err
-    assert "Using experiment project structure:" in out
-    assert "Created script.py and data" in out
 
 
 @pytest.mark.timeout(5, func_only=True)
@@ -309,8 +308,8 @@ def test_init_default(tmp_dir, scm, dvc, interactive, overrides, inp, capsys):
                 "python script.py\n"
                 "script.py\n"
                 "data\n"
-                "models\n"
                 "params.yaml\n"
+                "models\n"
                 "dvclive\n"
                 "y"
             ),
@@ -321,8 +320,8 @@ def test_init_default(tmp_dir, scm, dvc, interactive, overrides, inp, capsys):
             io.StringIO(
                 "script.py\n"
                 "data\n"
-                "models\n"
                 "params.yaml\n"
+                "models\n"
                 "dvclive\n"
                 "y"
             ),
@@ -375,11 +374,10 @@ def test_init_interactive_live(
 
     out, err = capsys.readouterr()
 
+    assert not out
     if interactive:
         assert "'script.py' does not exist, the file will be created." in err
         assert "'data' does not exist, the directory will be created." in err
-    assert "Using experiment project structure:" in out
-    assert "Created script.py and data" in out
 
 
 @pytest.mark.parametrize(
diff --git a/tests/unit/command/test_experiments.py b/tests/unit/command/test_experiments.py
--- a/tests/unit/command/test_experiments.py
+++ b/tests/unit/command/test_experiments.py
@@ -18,7 +18,6 @@
 from dvc.commands.experiments.show import CmdExperimentsShow, show_experiments
 from dvc.exceptions import InvalidArgumentError
 from dvc.repo import Repo
-from dvc.stage import PipelineStage
 from tests.utils import ANY
 from tests.utils.asserts import called_once_with_subset
 
@@ -669,9 +668,9 @@ def test_show_experiments_sort_by(capsys, sort_order):
 
 @pytest.mark.parametrize("extra_args", [(), ("--run",)])
 def test_experiments_init(dvc, scm, mocker, capsys, extra_args):
+    stage = mocker.Mock(outs=[], addressing="train")
     m = mocker.patch(
-        "dvc.repo.experiments.init.init",
-        return_value=PipelineStage(dvc, path="dvc.yaml", name="stage1"),
+        "dvc.repo.experiments.init.init", return_value=(stage, [])
     )
     runner = mocker.patch("dvc.repo.experiments.run.run", return_value=0)
     cli_args = parse_args(["exp", "init", *extra_args, "cmd"])
@@ -696,22 +695,20 @@ def test_experiments_init(dvc, scm, mocker, capsys, extra_args):
         interactive=False,
         force=False,
     )
-    expected = "Created train stage in dvc.yaml."
-    if not extra_args:
-        expected += (
-            ' To run, use "dvc exp run".\n' "See https://s.dvc.org/g/exp/run."
-        )
-    assert capsys.readouterr() == (expected + "\n", "")
+
     if extra_args:
         # `parse_args` creates a new `Repo` object
-        runner.assert_called_once_with(ANY(Repo), targets=["stage1"])
+        runner.assert_called_once_with(ANY(Repo), targets=["train"])
 
 
 def test_experiments_init_config(dvc, scm, mocker):
     with dvc.config.edit() as conf:
         conf["exp"] = {"code": "new_src", "models": "new_models"}
 
-    m = mocker.patch("dvc.repo.experiments.init.init")
+    stage = mocker.Mock(outs=[])
+    m = mocker.patch(
+        "dvc.repo.experiments.init.init", return_value=(stage, [])
+    )
     cli_args = parse_args(["exp", "init", "cmd"])
     cmd = cli_args.func(cli_args)
 
@@ -738,7 +735,10 @@ def test_experiments_init_config(dvc, scm, mocker):
 
 
 def test_experiments_init_explicit(dvc, mocker):
-    m = mocker.patch("dvc.repo.experiments.init.init")
+    stage = mocker.Mock(outs=[])
+    m = mocker.patch(
+        "dvc.repo.experiments.init.init", return_value=(stage, [])
+    )
     cli_args = parse_args(["exp", "init", "--explicit", "cmd"])
     cmd = cli_args.func(cli_args)
 
@@ -769,7 +769,10 @@ def test_experiments_init_cmd_not_required_for_interactive_mode(dvc, mocker):
     cmd = cli_args.func(cli_args)
     assert isinstance(cmd, CmdExperimentsInit)
 
-    m = mocker.patch("dvc.repo.experiments.init.init")
+    stage = mocker.Mock(outs=[])
+    m = mocker.patch(
+        "dvc.repo.experiments.init.init", return_value=(stage, [])
+    )
     assert cmd.run() == 0
     assert called_once_with_subset(m, ANY(Repo), interactive=True)
 
@@ -822,7 +825,10 @@ def test_experiments_init_extra_args(extra_args, expected_kw, mocker):
     cmd = cli_args.func(cli_args)
     assert isinstance(cmd, CmdExperimentsInit)
 
-    m = mocker.patch("dvc.repo.experiments.init.init")
+    stage = mocker.Mock(outs=[])
+    m = mocker.patch(
+        "dvc.repo.experiments.init.init", return_value=(stage, [])
+    )
     assert cmd.run() == 0
     assert called_once_with_subset(m, ANY(Repo), **expected_kw)
 
@@ -832,6 +838,40 @@ def test_experiments_init_type_invalid_choice():
         parse_args(["exp", "init", "--type=invalid", "cmd"])
 
 
+@pytest.mark.parametrize("args", [[], ["--run"]])
+def test_experiments_init_displays_output_on_no_run(dvc, mocker, capsys, args):
+    stage = dvc.stage.create(
+        name="train",
+        cmd=["cmd"],
+        deps=["code", "data"],
+        params=["params.yaml"],
+        outs=["metrics.json", "plots", "models"],
+    )
+    mocker.patch(
+        "dvc.repo.experiments.init.init", return_value=(stage, stage.deps)
+    )
+    mocker.patch("dvc.repo.experiments.run.run", return_value=0)
+    cli_args = parse_args(["exp", "init", "cmd", *args])
+    cmd = cli_args.func(cli_args)
+    assert cmd.run() == 0
+
+    expected_lines = [
+        "Creating dependencies: code, data and params.yaml",
+        "Creating train stage in dvc.yaml",
+        "",
+        "Ensure your experiment command creates "
+        "metrics.json, plots and models.",
+    ]
+    if not cli_args.run:
+        expected_lines += [
+            'You can now run your experiment using "dvc exp run".',
+        ]
+
+    out, err = capsys.readouterr()
+    assert not err
+    assert out.splitlines() == expected_lines
+
+
 def test_show_experiments_pcp(tmp_dir, mocker):
     all_experiments = {
         "workspace": {